Merged in feature/licensing (pull request #50)

Feature/licensing
This commit is contained in:
Christian Cueni 2020-05-12 11:37:17 +00:00
commit ce299e0f0e
104 changed files with 4488 additions and 896 deletions

View File

@ -4,6 +4,7 @@ const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {
NODE_ENV: '"development"', NODE_ENV: '"development"',
HEP_URL: JSON.stringify(process.env.HEP_URL),
MATOMO_HOST: JSON.stringify(process.env.MATOMO_HOST), MATOMO_HOST: JSON.stringify(process.env.MATOMO_HOST),
MATOMO_SITE_ID: JSON.stringify(process.env.MATOMO_SITE_ID), MATOMO_SITE_ID: JSON.stringify(process.env.MATOMO_SITE_ID),
}); });

View File

@ -1,6 +1,7 @@
'use strict' 'use strict'
module.exports = { module.exports = {
NODE_ENV: '"production"', NODE_ENV: '"production"',
HEP_URL: JSON.stringify(process.env.HEP_URL),
MATOMO_HOST: JSON.stringify(process.env.MATOMO_HOST), MATOMO_HOST: JSON.stringify(process.env.MATOMO_HOST),
MATOMO_SITE_ID: JSON.stringify(process.env.MATOMO_SITE_ID), MATOMO_SITE_ID: JSON.stringify(process.env.MATOMO_SITE_ID),
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,7 @@ describe('The Login Page', () => {
it('login and redirect to main page', () => { it('login and redirect to main page', () => {
const username = 'test'; const username = 'test';
const password = 'test'; const password = 'test';
cy.visit('/beta-login');
cy.visit('/');
cy.login(username, password, true); cy.login(username, password, true);
cy.get('body').contains('Neues Wissen erwerben'); cy.get('body').contains('Neues Wissen erwerben');
}); });
@ -12,7 +11,7 @@ describe('The Login Page', () => {
const username = ''; const username = '';
const password = 'test'; const password = 'test';
cy.visit('/'); cy.visit('/beta-login');
cy.login(username, password); cy.login(username, password);
cy.get('[data-cy=email-local-errors]').contains('E-Mail ist ein Pflichtfeld'); cy.get('[data-cy=email-local-errors]').contains('E-Mail ist ein Pflichtfeld');
}); });
@ -21,7 +20,7 @@ describe('The Login Page', () => {
const username = 'test'; const username = 'test';
const password = ''; const password = '';
cy.visit('/'); cy.visit('/beta-login');
cy.login(username, password); cy.login(username, password);
cy.get('[data-cy=password-local-errors]').contains('Passwort ist ein Pflichtfeld'); cy.get('[data-cy=password-local-errors]').contains('Passwort ist ein Pflichtfeld');
}); });
@ -30,23 +29,18 @@ describe('The Login Page', () => {
const username = 'test'; const username = 'test';
const password = '12345'; const password = '12345';
cy.visit('/'); cy.visit('/beta-login');
cy.login(username, password); cy.login(username, password);
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.'); cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
}); });
it('redirect after login', () => { it('logs out then logs in again', () => {
const username = 'test';
const password = 'test';
cy.visit('/book/topic/berufliche-grundbildung'); const user = 'rahel.cueni';
cy.login(username, password); const pw = 'test'
cy.get('body').contains('Berufliche Grundbildung');
});
it.only('logs out then logs in again', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.apolloLogin('rahel.cueni', 'test'); cy.apolloLogin(user, pw);
cy.visit('/me/my-class'); cy.visit('/me/my-class');
cy.get('[data-cy=header-user-widget]').should('exist').within(() => { cy.get('[data-cy=header-user-widget]').should('exist').within(() => {
cy.get('[data-cy=user-widget-avatar]').should('exist').click(); cy.get('[data-cy=user-widget-avatar]').should('exist').click();
@ -54,7 +48,11 @@ describe('The Login Page', () => {
cy.get('[data-cy=logout]').click(); cy.get('[data-cy=logout]').click();
cy.login('rahel.cueni', 'test'); cy.get('[data-cy=email-input]').should('exist').within(() => {
cy.visit('/beta-login');
});
cy.login(user, pw);
cy.get('[data-cy=header-user-widget]').should('exist').within(() => { cy.get('[data-cy=header-user-widget]').should('exist').within(() => {
cy.get('[data-cy=user-widget-avatar]').should('exist').click(); cy.get('[data-cy=user-widget-avatar]').should('exist').click();

View File

@ -0,0 +1,76 @@
import { GraphQLError } from "graphql";
const schema = require('../fixtures/schema.json');
describe('Email Verifcation', () => {
beforeEach(() => {
cy.server();
});
it('forwards to homepage if confirmation key is correct', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Coupon: {
coupon: {
success: true
}
},
}
});
cy.login('rahel.cueni', 'test', true)
cy.get('[data-cy="rooms-link"]').contains('Alle Räume anzeigen');
cy.visit('/license-activation');
cy.redeemCoupon('12345asfd');
cy.get('body').contains('Neues Wissen erwerben');
});
it('displays error if input is missing', () => {
cy.viewport('macbook-15');
cy.login('rahel.cueni', 'test', true)
cy.get('[data-cy="rooms-link"]').contains('Alle Räume anzeigen');
cy.visit('/license-activation');
cy.redeemCoupon('');
cy.get('[data-cy="coupon-local-errors"]').contains('Coupon ist ein Pflichtfeld.');
});
it('displays error if coupon input is wrong', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Coupon: new GraphQLError('invalid_coupon')
}
});
cy.login('rahel.cueni', 'test', true)
cy.get('[data-cy="rooms-link"]').contains('Alle Räume anzeigen');
cy.visit('/license-activation');
cy.redeemCoupon('12345asfd');
cy.get('[data-cy="coupon-remote-errors"]').contains('Der angegebene Coupon-Code ist ungültig.');
});
it('displays error if an error occures', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Coupon: new GraphQLError("unknown_error")
}
});
cy.login('rahel.cueni', 'test', true)
cy.get('[data-cy="rooms-link"]').contains('Alle Räume anzeigen');
cy.visit('/license-activation');
cy.redeemCoupon('12345asfd');
cy.get('[data-cy="coupon-remote-errors"]').contains('Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.');
});
});

View File

@ -0,0 +1,94 @@
const schema = require('../fixtures/schema_public.json');
describe('Email Verifcation', () => {
beforeEach(() => {
cy.server();
});
it('forwards to homepage if confirmation key is correct', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Registration: {
registration: {
message: "success",
success: true
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
// user should be logged in at that stage. As the cookie cannot be set at the right time
// we just check if the user gets redirected to the login page as we can't log her in
cy.url().should('include', 'hello?redirect=%2F');
});
it('displays error if key is incorrect', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: "invalid_key",
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
cy.get('[data-cy="code-nok-msg"]').contains('Der angegebene Verifizierungscode ist ungültig oder abgelaufen.');
cy.get('[data-cy="code-ok-msg"]').should('not.exist');
});
it('displays error if an error occured', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: "unkown_error",
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
cy.get('[data-cy="code-nok-msg"]').contains('Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.');
});
it('forwards to coupon page if user has no valid license', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
// endpoint: '/api/graphql'
operations: {
Registration: {
registration: {
message: "no_valid_license",
success: false
}
},
}
});
cy.visit('/verify-email?confirmation=abcd1234&id=12');
// user should be logged in at that stage. As the cookie cannot be set at the right time
// we just check if the user gets redirected to the coupon page as we can't log her in
cy.url().should('include', 'hello?redirect=%2Flicense-activation');
});
});

View File

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

View File

@ -0,0 +1,73 @@
const schema = require('../fixtures/schema_public.json');
const isEmailAvailableUrl = '**/rest/deutsch/V1/customers/isEmailAvailable';
const checkPasswordUrl = '**/rest/deutsch/V1/integration/customer/token';
describe('Login', () => {
beforeEach(() => {
cy.server();
});
it('works with valid email and password', () => {
cy.viewport('macbook-15');
cy.mockGraphql({
schema: schema,
operations: {
Login: variables => {
return {
login: {
errors: [],
message: "success",
success: true
}
}
},
}
});
cy.route('POST', isEmailAvailableUrl, 'false');
cy.route({
method: 'POST',
url: checkPasswordUrl,
response: 'token12345ABCD+',
});
cy.visit('/hello');
cy.checkEmailAvailable('feuz@aebi.ch');
cy.get('[data-cy="login-title"]').contains('Bitte geben Sie das passende Passwort ein');
cy.enterPassword('abcd1234');
// As we cannot set the cookie in the right manner, we just check for the absence of errors.
// In real world the user gets redirect to another page
cy.get('[data-cy="email-local-errors"]').should('not.exist');
});
it('displays error message if password is wrong', () => {
cy.viewport('macbook-15');
cy.route('POST', isEmailAvailableUrl, 'false');
cy.route({
method: 'POST',
status: 401,
response: {
message: "Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vor\u00fcbergehend deaktiviert."
},
url: checkPasswordUrl
});
cy.visit('/hello');
cy.checkEmailAvailable('feuz@aebi.ch');
cy.get('[data-cy="login-title"]').contains('Bitte geben Sie das passende Passwort ein');
cy.enterPassword('abcd1234');
cy.get('[data-cy="password-errors"]').contains('Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vorübergehend deaktiviert.');
});
it('displays error message if input is not an email address', () => {
cy.viewport('macbook-15');
cy.visit('/hello');
cy.checkEmailAvailable('feuzaebi.ch');
cy.get('[data-cy="email-local-errors"]').contains('Bitte geben Sie eine gülitge E-Mail an');
})
});

View File

@ -1,77 +0,0 @@
describe('The Regstration Page', () => {
// works locally, but not in pipelines.
// it('register user', () => {
// let timestamp = Math.round((new Date()).getTime() / 1000);
// const firstname = 'pesche';
// const lastname = 'peschemann';
// const email = `skillboxtest${timestamp}@iterativ.ch`;
// const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
// cy.visit('/register');
// cy.register(firstname, lastname, email, licenseKey);
// cy.get('.reset__heading').contains('Schauen Sie in Ihr Postfach');
// });
it('user sees error message if firstname is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = '';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="firstname-local-errors"]').contains('Vorname ist ein Pflichtfeld.');
});
it('user sees error message if lastname is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = '';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="lastname-local-errors"]').contains('Nachname ist ein Pflichtfeld.');
});
it('user sees error message if email is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = ``;
const licenseKey = 'c1fa2e2a-2e27-480d-8469-2e88414c4ad8';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="email-local-errors"]').contains('E-Mail ist ein Pflichtfeld.');
});
it('user sees error message if license is omitted', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = '';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="licenseKey-local-errors"]').contains('Lizenz ist ein Pflichtfeld.');
});
it('user sees error message if license key is wrong', () => {
let timestamp = Math.round((new Date()).getTime() / 1000);
const firstname = 'pesche';
const lastname = 'peschemann';
const email = `skillboxtest${timestamp}@iterativ.ch`;
const licenseKey = 'asdsafsadfsadfasdf';
cy.visit('/register');
cy.register(firstname, lastname, email, licenseKey);
cy.get('[data-cy="licenseKey-remote-errors"]').contains('Die angegebenen Lizenz ist unglültig');
});
})

View File

@ -0,0 +1,152 @@
const isEmailAvailableUrl = 'https://stage.hep-verlag.ch/rest/deutsch/V1/customers/isEmailAvailable';
const registerUrl = '/api/proxy/registration/';
let registrationResponse = {
id: 84215,
group_id: 1,
confirmation: "91cf39007547feae7e33778d89fc71db",
created_at: "2020-02-06 13:56:54",
updated_at: "2020-02-06 13:56:54",
created_in: "hep verlag",
email: "feuz@aebi.ch",
firstname: "Kari",
lastname: "Feuz",
prefix: "Herr",
gender: 1,
store_id: 1,
website_id: 1,
addresses: []
};
describe('Registration', () => {
beforeEach(() => {
cy.viewport('macbook-15');
cy.server();
});
it('works with valid data', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="email-check"]').contains('Eine Email ist auf dem Weg, bitte überprüfen sie ihre E-mail Konto.');
});
it('displays error if firstname is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, '', registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="firstname-local-errors"]').contains('Vorname ist ein Pflichtfeld');
});
it('displays error if lastname is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, '', 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="lastname-local-errors"]').contains('Nachname ist ein Pflichtfeld');
});
it('displays error if street is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, '', 'Bern', '3001', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="street-local-errors"]').contains('Strasse ist ein Pflichtfeld');
});
it('displays error if city is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', '', '3001', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="city-local-errors"]').contains('Ort ist ein Pflichtfeld');
});
it('displays error if postcode is missing', () => {
cy.route('POST', isEmailAvailableUrl, 'true');
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '', 'Abcd1234!', 'Abcd1234!');
cy.get('[data-cy="postcode-local-errors"]').contains('Postleitzahl ist ein Pflichtfeld');
});
it('displays error if password is missing', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', '', 'Abcd1234!');
cy.get('[data-cy="password-local-errors"]').contains('Passwort ist ein Pflichtfeld');
});
it('displays error if passwords are not secure', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234', 'Abcd1234');
cy.get('[data-cy="password-local-errors"]').contains('Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten und mindestens 8 Zeichen lang sein');
});
it('displays error if passwords are too short', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd12!', 'Abcd12!');
cy.get('[data-cy="password-local-errors"]').contains('Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten und mindestens 8 Zeichen lang sein');
});
it('displays error if passwords are not matching', () => {
cy.route('POST', isEmailAvailableUrl, "true");
cy.route('POST', registerUrl, registrationResponse);
cy.visit('/hello');
cy.checkEmailAvailable(registrationResponse.email);
cy.get('[data-cy="registration-title"]').contains('Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.');
cy.register(registrationResponse.gender, registrationResponse.firstname, registrationResponse.lastname, 'Weg 1', 'Bern', '3001', 'Abcd1234!', 'Abcd129999!');
cy.get('[data-cy="passwordConfirmation-local-errors"]').contains('Die Bestätigung von Passwort wiederholen stimmt nicht überein');
});
it('redirects to hello if email is missing', () => {
cy.visit('/register');
cy.get('[data-cy="hello-title"]').contains('Wollen sie mySkillbox jetzt im Unterricht verwenden?');
});
});

View File

@ -3,8 +3,8 @@ describe('The Room Page', () => {
// todo: mock all the graphql queries and mutations // todo: mock all the graphql queries and mutations
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.apolloLogin('rahel.cueni', 'test');
cy.visit('/room/ein-historisches-festival'); cy.visit('/room/ein-historisches-festival');
cy.login('rahel.cueni', 'test');
cy.get('[data-cy=add-room-entry-button]').click(); cy.get('[data-cy=add-room-entry-button]').click();
cy.get('.add-content-element:first-of-type').click(); cy.get('.add-content-element:first-of-type').click();

View File

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

View File

@ -29,16 +29,17 @@
// import 'cypress-graphql-mock'; // import 'cypress-graphql-mock';
import '@iam4x/cypress-graphql-mock'; import '@iam4x/cypress-graphql-mock';
Cypress.Commands.add('apolloLogin', (username, password) => { Cypress.Commands.add('apolloLogin', (username, password) => {
const payload = { const payload = {
'operationName': 'Login', 'operationName': 'BetaLogin',
'variables': { 'variables': {
'input': { 'input': {
'usernameInput': username, 'usernameInput': username,
'passwordInput': password 'passwordInput': password
} }
}, },
'query': 'mutation Login($input: LoginInput!) {\n login(input: $input) {\n success\n errors {\n field\n __typename\n }\n __typename\n }\n}\n' 'query': 'mutation BetaLogin($input: BetaLoginInput!) {\n betaLogin(input: $input) {\n success\n __typename\n }\n}\n'
}; };
cy.request({ cy.request({
@ -53,7 +54,7 @@ Cypress.Commands.add('apolloLogin', (username, password) => {
// todo: replace with apollo call // todo: replace with apollo call
Cypress.Commands.add("login", (username, password, visitLogin = false) => { Cypress.Commands.add("login", (username, password, visitLogin = false) => {
if (visitLogin) { if (visitLogin) {
cy.visit('/login'); cy.visit('/beta-login');
} }
if (username != '') { if (username != '') {
@ -110,22 +111,53 @@ Cypress.Commands.add('changePassword', (oldPassword, newPassword) => {
cy.get('[data-cy=change-password-button]').click(); cy.get('[data-cy=change-password-button]').click();
}); });
Cypress.Commands.add('register', (firstname, lastname, email, licenseKey) => { Cypress.Commands.add('checkEmailAvailable', (email) => {
if (firstname != '') { cy.get('[data-cy="email-input"]').type(email);
cy.get('[data-cy=firstname-input]').type(firstname); cy.get('[data-cy="hello-button"]').click();
}
if (lastname != '') {
cy.get('[data-cy=lastname-input]').type(lastname);
}
if (email != '') {
cy.get('[data-cy=email-input]').type(email);
}
if (licenseKey != '') {
cy.get('[data-cy=licenseKey-input]').type(licenseKey);
}
cy.get('[data-cy=register-button]').click();
}); });
Cypress.Commands.add('enterPassword', (password) => {
cy.get('[data-cy="password-input"]').type(password);
cy.get('[data-cy="login-button"]').click();
});
Cypress.Commands.add('register', (prefix, firstname, lastname, street, city, postcode, password, passwordConfirmation) => {
let selection = prefix === 1 ? 'Herr' : 'Frau';
cy.get('[data-cy="prefix-selection"]').select(selection);
if (firstname !== '') {
cy.get('[data-cy="firstname-input"]').type(firstname);
}
if (lastname !== '') {
cy.get('[data-cy="lastname-input"]').type(lastname);
}
if (street !== '') {
cy.get('[data-cy="street-input"]').type(street);
}
if (city !== '') {
cy.get('[data-cy="city-input"]').type(city);
}
if (postcode !== '') {
cy.get('[data-cy="postcode-input"]').type(postcode);
}
if (password !== '') {
cy.get('[data-cy="password-input"]').type(password);
}
cy.get('[data-cy="passwordConfirmation-input"]').type(passwordConfirmation);
cy.get('[data-cy="register-button"]').click();
});
Cypress.Commands.add('redeemCoupon', coupon => {
if (coupon !== '') {
cy.get('[data-cy="coupon-input"]').type(coupon);
}
cy.get('[data-cy="coupon-button"]').click();
})

187
client/package-lock.json generated
View File

@ -2460,7 +2460,7 @@
}, },
"chalk": { "chalk": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -2492,7 +2492,7 @@
}, },
"onetime": { "onetime": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true "dev": true
}, },
@ -9595,9 +9595,9 @@
} }
}, },
"fstream": { "fstream": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": { "requires": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"inherits": "~2.0.0", "inherits": "~2.0.0",
@ -9810,13 +9810,20 @@
} }
}, },
"globule": { "globule": {
"version": "1.2.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz",
"integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==",
"requires": { "requires": {
"glob": "~7.1.1", "glob": "~7.1.1",
"lodash": "~4.17.10", "lodash": "~4.17.12",
"minimatch": "~3.0.2" "minimatch": "~3.0.2"
},
"dependencies": {
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
}
} }
}, },
"graceful-fs": { "graceful-fs": {
@ -10425,9 +10432,9 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
}, },
"in-publish": { "in-publish": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz",
"integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ=="
}, },
"indent-string": { "indent-string": {
"version": "2.1.0", "version": "2.1.0",
@ -13334,7 +13341,7 @@
}, },
"chalk": { "chalk": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true, "dev": true,
"requires": { "requires": {
@ -13517,21 +13524,11 @@
"integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
"dev": true "dev": true
}, },
"lodash.assign": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
},
"lodash.camelcase": { "lodash.camelcase": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
}, },
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.create": { "lodash.create": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
@ -13576,11 +13573,6 @@
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
}, },
"lodash.mergewith": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ=="
},
"lodash.once": { "lodash.once": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -14177,7 +14169,8 @@
"nan": { "nan": {
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"optional": true
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
@ -14465,9 +14458,9 @@
} }
}, },
"node-sass": { "node-sass": {
"version": "4.9.2", "version": "4.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz",
"integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==", "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==",
"requires": { "requires": {
"async-foreach": "^0.1.3", "async-foreach": "^0.1.3",
"chalk": "^1.1.1", "chalk": "^1.1.1",
@ -14476,20 +14469,29 @@
"get-stdin": "^4.0.1", "get-stdin": "^4.0.1",
"glob": "^7.0.3", "glob": "^7.0.3",
"in-publish": "^2.0.0", "in-publish": "^2.0.0",
"lodash.assign": "^4.2.0", "lodash": "^4.17.15",
"lodash.clonedeep": "^4.3.2",
"lodash.mergewith": "^4.6.0",
"meow": "^3.7.0", "meow": "^3.7.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"nan": "^2.10.0", "nan": "^2.13.2",
"node-gyp": "^3.3.1", "node-gyp": "^3.8.0",
"npmlog": "^4.0.0", "npmlog": "^4.0.0",
"request": "2.87.0", "request": "^2.88.0",
"sass-graph": "^2.2.4", "sass-graph": "^2.2.4",
"stdout-stream": "^1.4.0", "stdout-stream": "^1.4.0",
"true-case-path": "^1.0.2" "true-case-path": "^1.0.2"
}, },
"dependencies": { "dependencies": {
"ajv": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
"integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ansi-styles": { "ansi-styles": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
@ -14516,10 +14518,80 @@
"which": "^1.2.9" "which": "^1.2.9"
} }
}, },
"fast-deep-equal": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"supports-color": { "supports-color": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
} }
} }
}, },
@ -17151,8 +17223,7 @@
"psl": { "psl": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ=="
"dev": true
}, },
"public-encrypt": { "public-encrypt": {
"version": "4.0.2", "version": "4.0.2",
@ -18592,9 +18663,9 @@
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}, },
"stdout-stream": { "stdout-stream": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
"integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
"requires": { "requires": {
"readable-stream": "^2.0.1" "readable-stream": "^2.0.1"
} }
@ -18805,12 +18876,12 @@
"integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=" "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI="
}, },
"tar": { "tar": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"requires": { "requires": {
"block-stream": "*", "block-stream": "*",
"fstream": "^1.0.2", "fstream": "^1.0.12",
"inherits": "2" "inherits": "2"
} }
}, },
@ -19098,25 +19169,11 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
}, },
"true-case-path": { "true-case-path": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
"integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
"requires": { "requires": {
"glob": "^6.0.4" "glob": "^7.1.2"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
} }
}, },
"tryer": { "tryer": {

View File

@ -57,7 +57,7 @@
"lodash": "^4.17.10", "lodash": "^4.17.10",
"moment": "^2.24.0", "moment": "^2.24.0",
"node-notifier": "^5.1.2", "node-notifier": "^5.1.2",
"node-sass": "^4.9.2", "node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "^3.2.0", "optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0", "ora": "^1.2.0",
"portfinder": "^1.0.13", "portfinder": "^1.0.13",

View File

@ -18,12 +18,16 @@ const writeLocalCache = cache => {
sidebar: { sidebar: {
__typename: 'Sidebar', __typename: 'Sidebar',
open: false open: false
} },
helloEmail: {
__typename: 'HelloEmail',
email: ''
},
} }
}); });
}; };
export default function (uri) { export default function (uri, networkErrorCallback) {
const httpLink = createHttpLink({ const httpLink = createHttpLink({
// uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/', // uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/',
uri, uri,
@ -58,6 +62,11 @@ export default function (uri) {
}); });
const errorLink = onError(({response, operation, networkError, graphQLErrors}) => { const errorLink = onError(({response, operation, networkError, graphQLErrors}) => {
if (networkError && networkErrorCallback) {
networkErrorCallback(networkError.statusCode);
return Observable.of();
}
if (graphQLErrors) { if (graphQLErrors) {
graphQLErrors.forEach(({message, locations, path}) => graphQLErrors.forEach(({message, locations, path}) =>
console.log( console.log(

View File

@ -7,6 +7,7 @@ fragment UserParts on UserNode {
firstName firstName
lastName lastName
avatarUrl avatarUrl
expiryDate
lastModule { lastModule {
id id
slug slug

View File

@ -0,0 +1,5 @@
query HelloEmail {
helloEmail @client {
email
}
}

View File

@ -0,0 +1,3 @@
mutation($helloEmail: String!) {
helloEmail(email: $helloEmail) @client
}

View File

@ -0,0 +1,6 @@
mutation BetaLogin($input: BetaLoginInput!) {
betaLogin(input: $input) {
success
message
}
}

View File

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

View File

@ -0,0 +1,5 @@
mutation Coupon($input: CouponInput!){
coupon(input: $input) {
success
}
}

View File

@ -1,8 +1,6 @@
mutation Registration($input: RegistrationInput!){ mutation Registration($input: RegistrationInput!) {
registration(input: $input) { registration(input: $input) {
success success
errors { message
field
}
} }
} }

View File

@ -1,4 +1,5 @@
import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql'; import SCROLL_POSITION from '@/graphql/gql/local/scrollPosition.gql';
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import SIDEBAR from '@/graphql/gql/local/sidebar.gql'; import SIDEBAR from '@/graphql/gql/local/sidebar.gql';
export const resolvers = { export const resolvers = {
@ -9,6 +10,12 @@ export const resolvers = {
cache.writeQuery({query: SCROLL_POSITION, data}); cache.writeQuery({query: SCROLL_POSITION, data});
return data.scrollPosition; return data.scrollPosition;
}, },
helloEmail: (_, {email}, {cache}) => {
const data = cache.readQuery({query: HELLO_EMAIL});
data.helloEmail.email = email;
cache.writeQuery({query: HELLO_EMAIL, data});
return data.helloEmail;
},
toggleSidebar: (_, {open}, {cache}) => { toggleSidebar: (_, {open}, {cache}) => {
const data = cache.readQuery({query: SIDEBAR}); const data = cache.readQuery({query: SIDEBAR});
data.sidebar.open = open; data.sidebar.open = open;

View File

@ -1,15 +1,24 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
export const typeDefs = gql` export const typeDefs = gql`
type ScrollPosition { type ScrollPosition {
scrollTo: String! scrollTo: String!
} }
type Sidebar { type HelloEmail {
email: String!
}
type Sidebar {
open: Boolean! open: Boolean!
} }
type Mutation { type Mutation {
scrollTo(scrollTo: String!): ScrollPosition scrollTo(scrollTo: String!): ScrollPosition
} }
type Mutation {
helloEmail(email: String!): HelloEmail
}
`; `;

View File

@ -0,0 +1,18 @@
import * as axios from 'axios'
const hepBaseUrl = process.env.HEP_URL;
export function register(registrationData) {
return axios.post('/api/proxy/registration/', registrationData);
}
export function login(username, password) {
return axios.post(`${hepBaseUrl}/rest/deutsch/V1/integration/customer/token`, {username, password});
}
export function emailExists(email) {
return axios.post(`${hepBaseUrl}/rest/deutsch/V1/customers/isEmailAvailable`, {
customerEmail: email,
websiteId: 1
});
}

View File

@ -1,17 +1,22 @@
<template> <template>
<div class="layout layout--public public"> <div class="layout layout--public public">
<logo class="public__logo"></logo> <div class="public__logo">
<router-view class="layout__content"></router-view> <logo></logo>
<footer class="layout__footer">Footer</footer> </div>
<router-view class="public__content"></router-view>
<default-footer class="skillbox__footer public__footer footer"></default-footer>
</div> </div>
</template> </template>
<script> <script>
import Logo from '@/components/icons/Logo'; import Logo from '@/components/icons/Logo';
import DefaultFooter from '@/layouts/DefaultFooter';
export default { export default {
components: {Logo}, components: {
Logo,
DefaultFooter
},
} }
</script> </script>
@ -20,17 +25,46 @@ import Logo from '@/components/icons/Logo';
@import "@/styles/_mixins.scss"; @import "@/styles/_mixins.scss";
@import "@/styles/_default-layout.scss"; @import "@/styles/_default-layout.scss";
.public { @mixin content-block {
padding-right: $medium-spacing;
padding-left: $medium-spacing;
max-width: 800px; max-width: 800px;
min-width: 320px; min-width: 320px;
padding-top: 4*$large-spacing; width: 100%;
margin: 0 auto;
}
.logo {
position: relative;
width: 260px;
height: 43px;
}
.public {
grid-template-areas: "h" "c" "f";
&__content {
@include content-block();
margin-bottom: $large-spacing;
}
&__logo { &__logo {
position: relative; @include content-block();
margin-top: $medium-spacing
}
width: 260px; &__footer {
height: 43px; background-color: $color-silver-light;
display: block;
} }
} }
.footer {
padding-top: $large-spacing;
&__content {
@include content-block();
}
}
</style> </style>

View File

@ -10,7 +10,7 @@ import router from './router'
import store from '@/store/index' import store from '@/store/index'
import VueScrollTo from 'vue-scrollto'; import VueScrollTo from 'vue-scrollto';
import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js'; import {Validator, install as VeeValidate} from 'vee-validate/dist/vee-validate.minimal.esm.js';
import {required, min} from 'vee-validate/dist/rules.esm.js'; import {required, min, decimal, confirmed} from 'vee-validate/dist/rules.esm.js';
import veeDe from 'vee-validate/dist/locale/de'; import veeDe from 'vee-validate/dist/locale/de';
import {dateFilter} from './filters/date-filter'; import {dateFilter} from './filters/date-filter';
import autoGrow from '@/directives/auto-grow' import autoGrow from '@/directives/auto-grow'
@ -45,8 +45,8 @@ if (process.env.MATOMO_HOST) {
Vue.directive('click-outside', clickOutside); Vue.directive('click-outside', clickOutside);
Vue.directive('auto-grow', autoGrow); Vue.directive('auto-grow', autoGrow);
const publicApolloClient = apolloClientFactory('/api/graphql-public/'); const publicApolloClient = apolloClientFactory('/api/graphql-public/', null);
const privateApolloClient = apolloClientFactory('/api/graphql/'); const privateApolloClient = apolloClientFactory('/api/graphql/', networkErrorCallback);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
clients: { clients: {
@ -57,6 +57,8 @@ const apolloProvider = new VueApollo({
Validator.extend('required', required); Validator.extend('required', required);
Validator.extend('min', min); Validator.extend('min', min);
Validator.extend('decimal', decimal);
Validator.extend('confirmed', confirmed);
const dict = { const dict = {
custom: { custom: {
@ -72,9 +74,10 @@ const dict = {
Validator.localize('de', veeDe) Validator.localize('de', veeDe)
Validator.localize('de', dict) Validator.localize('de', dict)
// https://github.com/baianat/vee-validate/issues/51 // https://github.com/baianat/vee-validate/issues/51
Validator.extend('strongPassword', { Validator.extend('strongPassword', {
getMessage: field => 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten', getMessage: field => 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten und mindestens 8 Zeichen lang sein',
validate: value => { validate: value => {
const strongRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*?(),.":{}|<>+])(?=.{8,})/; const strongRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*?(),.":{}|<>+])(?=.{8,})/;
return strongRegex.test(value); return strongRegex.test(value);
@ -112,28 +115,43 @@ function unauthorizedAccess(to) {
return loginRequired(to) && getCookieValue('loginStatus') !== 'true'; return loginRequired(to) && getCookieValue('loginStatus') !== 'true';
} }
function redirectUsersWithoutValidLicense(to) {
return privateApolloClient.query({
query: ME_QUERY,
}).then(({data}) => data.me.expiryDate == null);
}
function redirectStudentsWithoutClass() { function redirectStudentsWithoutClass() {
return privateApolloClient.query({ return privateApolloClient.query({
query: ME_QUERY, query: ME_QUERY,
}).then(({data}) => data.me.schoolClasses.edges.length === 0 && data.me.permissions.length === 0); }).then(({data}) => data.me.schoolClasses.edges.length === 0 && data.me.permissions.length === 0);
} }
function networkErrorCallback(statusCode) {
if (statusCode === 402) {
router.push({name: 'licenseActivation'}, 0);
}
}
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
// handle logout
if (to.path === '/logout') { if (to.path === '/logout') {
privateApolloClient.resetStore();
publicApolloClient.resetStore(); publicApolloClient.resetStore();
next({name: 'login'}); next({name: 'hello'});
return return
} }
if (unauthorizedAccess(to)) { if (unauthorizedAccess(to)) {
const redirectUrl = `/login?redirect=${to.path}`; const redirectUrl = `/hello?redirect=${to.path}`;
next(redirectUrl); next(redirectUrl);
return return;
} }
if (to.name !== 'join-class' && loginRequired(to) && await redirectStudentsWithoutClass()) { if (to.name !== 'licenseActivation' && loginRequired(to) && await redirectUsersWithoutValidLicense()) {
next({name: 'licenseActivation'})
return;
}
if ((to.name !== 'join-class' && to.name !== 'licenseActivation') && loginRequired(to) && await redirectStudentsWithoutClass()) {
next({name: 'join-class'}) next({name: 'join-class'})
return return
} }

View File

@ -0,0 +1,169 @@
<template>
<div class="login public-page">
<h1 class="login__title public-page__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'"
data-vv-as="E-Mail"
: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"
data-vv-as="Passwort"
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="account-link">
<p class="account-link__text">Haben Sie noch kein Konto?</p>
<router-link class="account-link__link text-link" :to="{name: 'registration'}">Jetzt registrieren
</router-link>
</div>
</form>
</div>
</template>
<script>
import BETA_LOGIN_MUTATION from '@/graphql/gql/mutations/betaLogin.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: BETA_LOGIN_MUTATION,
variables: {
input: {
usernameInput: this.email,
passwordInput: this.password
}
},
update(
store,
{
data: {
betaLogin
}
}
) {
try {
if (betaLogin.success) {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
that.$router.push(redirectUrl);
}
} catch (e) {
console.warn(e);
that.loginError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
}
}
}).catch(error => {
const firstError = error.graphQLErrors[0];
switch (firstError.message) {
case 'invalid_credentials':
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
break;
case 'license_inactive':
that.loginError = 'Ihre Lizenz ist nicht mehr aktiv.';
break;
}
});
}
});
},
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";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
display: flex;
justify-content: space-between;
&__reset {
display: inline-block;
margin-left: $large-spacing;
padding: 15px;
line-height: 19px;
}
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<div class="check-email">
<main class="check-email__content content" data-cy="email-check">
<p class="content__instructions">Eine Email ist auf dem Weg, bitte überprüfen sie ihre E-mail Konto.</p>
</main>
</div>
</template>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.content__instructions {
margin-top: $medium-spacing;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<div class="emailconfirmation public-page">
<h1 class="emailconfirmation__title public-page__title">Überprüfung der E-Mail Adresse</h1>
<p v-if="loading">Der Verifikationscode wird überprüft.</p>
<p v-if="showOkMessage" data-cy="code-ok-msg">Der Verifikationscode ist gültig. Sie werden weitergeleitet.</p>
<p v-if="showErrorMessage" data-cy="code-nok-msg">{{errorMessage}}</p>
</div>
</template>
<script>
import REGISTRATION_MUTATION from '@/graphql/gql/mutations/registration.gql';
export default {
data() {
return {
loading: true,
keyValid: false,
errorMessage: ''
};
},
computed: {
showOkMessage() {
return !this.loading && this.keyValid;
},
showErrorMessage() {
return !this.loading && !this.keyValid;
}
},
mounted() {
this.$apollo.mutate({
mutation: REGISTRATION_MUTATION,
client: 'publicClient',
variables: {
input: {
confirmationKey: this.$route.query.confirmation,
userId: this.$route.query.id
}
},
fetchPolicy: 'no-cache'
}).then(({data}) => {
this.loading = false;
if (data.registration.success) {
this.keyValid = true;
if (data.registration.message === 'no_valid_license') {
this.$router.push({name: 'licenseActivation'});
} else {
this.$router.push('/');
}
} else {
switch (data.registration.message) {
case 'invalid_key':
this.errorMessage = 'Der angegebene Verifizierungscode ist ungültig oder abgelaufen.';
break;
case 'no_valid_license':
this.$router.push({name: 'licenseActivation'});
break;
default:
this.errorMessage = 'Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.';
}
}
})
.catch(() => {
this.errorMessage = 'Ein Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator.'
});
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="forgot-password public-page">
<header class="info-header">
<h1 class="forgot-password__title public-page__title" data-cy="forgot-password">Passwort vergessen?</h1>
</header>
<section class="forgot-password__section forgot-password__text">
<p class="forgot-info">Ihr Benutzerkonto wird durch den Hep Verlag verwaltet und deshalb können Sie das Passwort ausschliesslicht auf
<a class="hep-link" href="https://www.hep-verlag.ch">www.hep-verlag.ch</a> verwaltet werden.</p>
<p class="forgot-info">Melden Sie sich mit der gleichen E-Mail-Adresse und dem gleichen </p>
</section>
<section class="forgot-password__section forgot-password__link">
<a class="button button--primary button--big actions__submit" href="https://www.hep-verlag.ch">Hep Verlag Webseite besuchen</a>
</section>
</div>
</template>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.forgot-info {
font-family: $sans-serif-font-family;
&:last-child {
margin-top: $large-spacing;
}
}
.forgot-password__link {
margin-top: $large-spacing;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="hello public-page">
<h1 class="hello__title public-page__title" data-cy="hello-title">Wollen sie mySkillbox jetzt im Unterricht verwenden?</h1>
<form class="hello__form hello-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="hello-form__field skillboxform-input">
<label for="email" class="skillboxform-input__label">E-Mail</label>
<input
id="email"
name="email"
type="email"
v-model="email"
v-validate="'required'"
data-vv-as="E-Mail"
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="email-input"
placeholder="E-Mail eingeben"
tabindex="0"
/>
<small
v-if="errors.has('email') && submitted"
class="skillboxform-input__error"
data-cy="email-local-errors"
>{{ errors.first('email') }}</small>
</div>
<div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="hello-button">Los geht's</button>
</div>
</form>
</div>
</template>
<script>
import {emailExists} from '../hep-client/index';
import HELLO_EMAIL_MUTATION from '@/graphql/gql/local/mutations/helloEmail.gql';
export default {
components: {},
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
if (result) {
emailExists(this.email).then((response) => {
let redirectRouteName = 'login';
if (response.data) {
redirectRouteName = 'registration';
}
this.$apollo.mutate({
mutation: HELLO_EMAIL_MUTATION,
variables: {
helloEmail: this.email
}
}).then(() => this.$router.push({name: redirectRouteName, query: this.$route.query}));
})
.catch(() => {
this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals.';
});
}
});
},
resetForm() {
this.email = '';
this.submitted = false;
this.$validator.reset();
}
},
data() {
return {
email: '',
submitted: false
};
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
</style>

View File

@ -21,7 +21,7 @@
<div> <div>
<a class="button button--primary button--big" data-cy="join-class" @click="joinClass(code)">Klasse beitreten</a> <a class="button button--primary button--big" data-cy="join-class" @click="joinClass(code)">Klasse beitreten</a>
<a class="button button--big" data-cy="join-class-cancel" @click="cancel">Abbrechen</a> <button class="button button--big" data-cy="join-class-cancel" @click="logout">Abmelden</button>
</div> </div>
</div> </div>
@ -31,6 +31,7 @@
<script> <script>
import JOIN_CLASS_MUTATION from '@/graphql/gql/mutations/joinClass.gql'; import JOIN_CLASS_MUTATION from '@/graphql/gql/mutations/joinClass.gql';
import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/mySchoolClass'; import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/mySchoolClass';
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
import addSchoolClassMixin from '@/mixins/add-school-class'; import addSchoolClassMixin from '@/mixins/add-school-class';
@ -74,8 +75,12 @@
} }
}) })
}, },
cancel() { logout() {
this.$router.go(-1); this.$apollo.mutate({
mutation: LOGOUT_MUTATION,
}).then(({data}) => {
if (data.logout.success) { location.replace('/') }
});
} }
} }
} }

View File

@ -0,0 +1,145 @@
<template>
<div class="license-activation public-page">
<header class="info-header">
<p class="info-header__text small-emph">Für <span class="info-header__emph">{{me.email}}</span> haben wir keine gültige Lizenz gefunden</p>
</header>
<section class="coupon">
<form class="license-activation__form license-activation-form" novalidate @submit.prevent="validateBeforeSubmit">
<h2>Geben Sie einen Coupon-Code ein</h2>
<div class="change-form__field skillboxform-input">
<label for="coupon" class="skillboxform-input__label">Coupon-Code</label>
<input
id="coupon"
name="coupon"
type="coupon"
data-vv-as="Coupon"
v-model="coupon"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('coupon') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="coupon-input"
tabindex="0"
/>
<small
v-if="errors.has('coupon') && submitted"
class="skillboxform-input__error"
data-cy="coupon-local-errors"
>{{ errors.first('coupon') }}</small>
<small
v-for="error in couponErrors"
:key="error"
class="skillboxform-input__error"
data-cy="coupon-remote-errors"
>{{ error }}</small>
</div>
<div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="coupon-button">Coupon abschicken</button>
</div>
</form>
</section>
<section class="get-license">
<h2>Oder, kaufen Sie eine Lizenz</h2>
<ul class="license-links">
<li class="license-links__item"><a :href="teacherEditionUrl" class="hep-link">mySkillobx für Lehrpersonen</a></li>
<li class="license-links__item"><a :href="studentEditionUrl" class="hep-link">mySkillobx für Lernende</a></li>
</ul>
</section>
</div>
</template>
<script>
import me from '@/mixins/me';
import REDEEM_COUPON from '@/graphql/gql/mutations/redeemCoupon.gql';
import ME_QUERY from '@/graphql/gql/meQuery.gql';
export default {
mixins: [me],
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
let that = this;
if (result) {
this.$apollo.mutate({
mutation: REDEEM_COUPON,
variables: {
input: {
couponCode: this.coupon
}
},
update(
store,
{
data: {coupon}
}
) {
if (coupon.success) {
that.couponErrors = [];
that.$apollo.query({
query: ME_QUERY,
fetchPolicy: 'network-only',
}).then(() => that.$router.push('/'));
}
}
}).catch(({message}) => {
if (message.indexOf('invalid_coupon') > -1) {
that.couponErrors = ['Der angegebene Coupon-Code ist ungültig.'];
} else {
that.couponErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie es nochmals oder kontaktieren Sie den Administrator.'];
}
});
}
});
},
resetForm() {
this.coupon = '';
this.submitted = false;
this.$validator.reset();
}
},
data() {
return {
coupon: '',
couponErrors: [],
loginError: '',
submitted: false,
me: {
email: ''
},
teacherEditionUrl: `${process.env.HEP_URL}/myskillbox-lehrpersonen`,
studentEditionUrl: `${process.env.HEP_URL}/myskillbox-fur-lernende`
};
},
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
.get-license {
margin-top: $large-spacing
}
.license-links {
&__item {
margin-bottom: $medium-spacing;
}
}
</style>

View File

@ -1,33 +1,10 @@
<template> <template>
<div class="login public-page"> <div class="login public-page">
<h1 class="login__title public-page__title">Melden Sie sich jetzt an</h1> <header class="info-header">
<p class="info-header__text small-emph">Super wir haben für <span class="info-header__emph">{{helloEmail.email}}</span> ein Hep Konto gefunden</p>
<h1 class="login__title public-page__title" data-cy="login-title">Bitte geben Sie das passende Passwort ein</h1>
</header>
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit"> <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'"
data-vv-as="E-Mail"
: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"> <div class="change-form__field skillboxform-input">
<label for="pw" class="skillboxform-input__label">Passwort</label> <label for="pw" class="skillboxform-input__label">Passwort</label>
<input <input
@ -41,37 +18,29 @@
class="change-form__new skillbox-input skillboxform-input__input" class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off" autocomplete="off"
data-cy="password-input" data-cy="password-input"
tabindex="0"
/> />
<small
v-if="errors.has('password') && submitted"
class="skillboxform-input__error"
data-cy="password-local-errors"
>{{ errors.first('password') }}</small>
<small <small
v-for="error in passwordErrors" v-for="error in passwordErrors"
:key="error" :key="error"
class="skillboxform-input__error" class="skillboxform-input__error"
data-cy="password-remote-errors" data-cy="password-errors"
>{{ error }}</small> >{{ error }}</small>
</div> </div>
<div class="skillboxform-input">
<small class="skillboxform-input__error" data-cy="login-error" v-if="loginError">{{loginError}}</small>
</div>
<div class="actions"> <div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="login-button">Anmelden</button> <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> <router-link class="button button--big actions__submit back-button" :to="{name: 'hello'}">Abbrechen</router-link>
</div> <router-link class="actions__reset text-link" :to="{name: 'forgotPassword'}">Passwort vergessen?</router-link>
<div class="account-link">
<p class="account-link__text">Haben Sie noch kein Konto?</p>
<router-link class="account-link__link text-link" :to="{name: 'registration'}">Jetzt registrieren
</router-link>
</div> </div>
</form> </form>
</div> </div>
</template> </template>
<script> <script>
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
import LOGIN_MUTATION from '@/graphql/gql/mutations/login.gql'; import LOGIN_MUTATION from '@/graphql/gql/mutations/login.gql';
import {login} from '@/hep-client/index';
export default { export default {
components: {}, components: {},
@ -80,44 +49,73 @@ export default {
validateBeforeSubmit() { validateBeforeSubmit() {
this.$validator.validate().then(result => { this.$validator.validate().then(result => {
this.submitted = true; this.submitted = true;
let that = this;
if (result) { if (result) {
this.$apollo.mutate({ login(this.helloEmail.email, this.password)
client: 'publicClient', .then((response) => {
mutation: LOGIN_MUTATION, if (response.status === 200) {
variables: { this.mySkillboxLogin(response.data);
input: { } else {
usernameInput: this.email, this.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
passwordInput: this.password
}
},
fetchPolicy: 'no-cache',
}).then(({data: {login}}) => {
try {
if (login.success) {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
that.$router.push(redirectUrl);
} else {
const firstError = login.errors[0];
switch (firstError.field) {
case 'invalid_credentials':
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
break;
case 'license_inactive':
that.loginError = 'Ihre Lizenz ist nicht mehr aktiv.';
break;
}
}
} catch (e) {
console.warn(e);
that.loginError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
} }
})
.catch((error) => {
if (error.response.data.message && error.response.data.message === 'Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vor\u00fcbergehend deaktiviert.') {
this.passwordErrors = ['Sie haben sich nicht korrekt eingeloggt oder Ihr Konto ist vorübergehend deaktiviert.'];
} else {
this.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
}
}); });
} }
}); });
}, },
mySkillboxLogin(token) {
const that = this;
this.$apollo.mutate({
client: 'publicClient',
mutation: LOGIN_MUTATION,
variables: {
input: {
tokenInput: token
}
},
update(
store,
{
data: {
login
}
}
) {
try {
if (login.success) {
if (login.message === 'no_valid_license') {
that.$router.push({name: 'licenseActivation'})
} else {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/';
that.$router.push(redirectUrl);
}
}
} catch (e) {
that.passwordErrors = ['Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.'];
}
}
})
.catch(errors => {
const firstError = errors.graphQLErrors[0];
switch (firstError.message) {
case 'invalid_credentials':
that.passwordErrors = ['Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.'];
break;
case 'email_not_verified':
that.passwordErrors = ['Bitte verifiziere zuerst deine E-Mail.'];
break;
case 'no_valid_license':
this.$router.push({name: 'licenseActivation'})
break;
}
});
},
resetForm() { resetForm() {
this.email = '';
this.password = ''; this.password = '';
this.submitted = false; this.submitted = false;
this.$validator.reset(); this.$validator.reset();
@ -126,14 +124,22 @@ export default {
data() { data() {
return { return {
email: '',
password: '', password: '',
emailErrors: [],
passwordErrors: [], passwordErrors: [],
loginError: '',
submitted: false submitted: false
}; };
} },
apollo: {
helloEmail: {
query: HELLO_EMAIL,
result({data: {helloEmail}}) {
if (helloEmail.email === '') {
this.$router.push({name: 'hello'});
}
}
},
},
}; };
</script> </script>
@ -147,10 +153,20 @@ export default {
} }
.actions { .actions {
display: flex;
&__reset { &__reset {
display: inline-block; display: inline-block;
margin-left: $large-spacing; margin-left: auto;
line-height: 19px;;
display: inline-block;
padding: $small-spacing;
} }
} }
.back-button {
font-size: 1rem;
line-height: normal;
margin-left: $medium-spacing;
}
</style> </style>

View File

@ -1,8 +1,28 @@
<template> <template>
<div class="registration public-page"> <div class="registration public-page">
<h1 class="registration__title public-page__title">Registrieren Sie ihr persönliches Konto.</h1> <header class="info-header">
<p class="info-header__text small-emph">Für <span class="info-header__emph">{{helloEmail}}</span> haben wir kein Hep Konto gefunden.</p>
<h1 class="registration__title public-page__title" data-cy="registration-title">Damit Sie mySkillbox verwenden können, müssen Sie ein Konto erstellen.</h1>
</header>
<form class="registration__form registration-form" novalidate @submit.prevent="validateBeforeSubmit"> <form class="registration__form registration-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="registration-form__field skillboxform-input"> <div class="registration-form__field skillboxform-input">
<div class="registration-form__field skillboxform-input">
<label for="prefix" class="skillboxform-input__label">Anrede</label>
<select
id="prefix"
name="prefix"
v-model="prefix"
data-vv-as="Prefix"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('prefix') }"
class="change-form__prefix skillbox-input skillboxform-input__input skillbox-dropdown"
autocomplete="off"
data-cy="prefix-selection"
>
<option value="Herr" selected>Herr</option>
<option value="Frau">Frau</option>
</select>
</div>
<label for="firstname" class="skillboxform-input__label">Vorname</label> <label for="firstname" class="skillboxform-input__label">Vorname</label>
<input <input
id="firstname" id="firstname"
@ -55,74 +75,162 @@
>{{ error }}</small> >{{ error }}</small>
</div> </div>
<div class="change-form__field skillboxform-input"> <div class="change-form__field skillboxform-input">
<label for="email" class="skillboxform-input__label">E-Mail</label> <label for="street" class="skillboxform-input__label">Strasse</label>
<input <input
id="email" id="street"
name="email" name="street"
type="text" type="text"
v-model="email" v-model="street"
data-vv-as="E-Mail" data-vv-as="Strasse"
v-validate="'required|email'" v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('email') }" :class="{ 'skillboxform-input__input--error': errors.has('street') }"
class="change-form__new skillbox-input skillboxform-input__input" class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off" autocomplete="off"
data-cy="email-input" data-cy="street-input"
/> />
<small <small
v-if="errors.has('email') && submitted" v-if="errors.has('street') && submitted"
class="skillboxform-input__error" class="skillboxform-input__error"
data-cy="email-local-errors" data-cy="street-local-errors"
>{{ errors.first('email') }}</small> >{{ errors.first('street') }}</small>
<small <small
v-for="error in emailErrors" v-for="error in streetErrors"
:key="error" :key="error"
class="skillboxform-input__error" class="skillboxform-input__error"
data-cy="email-remote-errors" data-cy="street-remote-errors"
>{{ error }}</small> >{{ error }}</small>
</div> </div>
<div class="change-form__field skillboxform-input"> <div class="change-form__field skillboxform-input">
<label for="licenseKey" class="skillboxform-input__label">Lizenz</label> <label for="city" class="skillboxform-input__label">Ort</label>
<input <input
id="licenseKey" id="city"
name="licenseKey" name="city"
type="text" type="text"
v-model="licenseKey" v-model="city"
data-vv-as="Lizenz" data-vv-as="Ort"
v-validate="'required'" v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('licenseKey') }" :class="{ 'skillboxform-input__input--error': errors.has('city') }"
class="change-form__new skillbox-input skillboxform-input__input" class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off" autocomplete="off"
data-cy="licenseKey-input" data-cy="city-input"
/> />
<small <small
v-if="errors.has('licenseKey') && submitted" v-if="errors.has('city') && submitted"
class="skillboxform-input__error" class="skillboxform-input__error"
data-cy="licenseKey-local-errors" data-cy="city-local-errors"
>{{ errors.first('licenseKey') }}</small> >{{ errors.first('city') }}</small>
<small <small
v-for="error in licenseKeyErrors" v-for="error in cityErrors"
:key="error" :key="error"
class="skillboxform-input__error" class="skillboxform-input__error"
data-cy="licenseKey-remote-errors" data-cy="city-remote-errors"
>{{ error }}</small> >{{ error }}</small>
</div> </div>
<div class="change-form__field skillboxform-input">
<label for="postcode" class="skillboxform-input__label">Postleitzahl</label>
<input
id="postcode"
name="postcode"
type="text"
v-model="postcode"
data-vv-as="Postleitzahl"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('postcode') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="postcode-input"
/>
<small
v-if="errors.has('postcode') && submitted"
class="skillboxform-input__error"
data-cy="postcode-local-errors"
>{{ errors.first('postcode') }}</small>
<small
v-for="error in postcodeErrors"
:key="error"
class="skillboxform-input__error"
data-cy="postcode-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="password" class="skillboxform-input__label">Passwort</label>
<input
id="password"
name="password"
type="text"
v-model="password"
data-vv-as="Passwort"
v-validate="'required|strongPassword'"
:class="{ 'skillboxform-input__input--error': errors.has('password') && submitted }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="password-input"
ref="password"
/>
<small
v-if="errors.has('password') && submitted"
class="skillboxform-input__error"
data-cy="password-local-errors"
>{{ errors.first('password') }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="password2" class="skillboxform-input__label">Passwort wiederholen</label>
<input
id="passwordConfirmation"
name="passwordConfirmation"
type="text"
v-model="passwordConfirmation"
data-vv-as="Passwort wiederholen"
v-validate="'required|confirmed:password'"
:class="{ 'skillboxform-input__input--error': errors.has('passwordConfirmation') && submitted }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="passwordConfirmation-input"
/>
<small
v-if="errors.has('passwordConfirmation') && submitted"
class="skillboxform-input__error"
data-cy="passwordConfirmation-local-errors"
>{{ errors.first('passwordConfirmation') }}</small>
</div>
<div class="skillboxform-input"> <div class="skillboxform-input">
<small class="skillboxform-input__error" data-cy="registration-error" v-if="registrationError">{{registrationError}}</small> <small class="skillboxform-input__error" data-cy="registration-error" v-if="registrationError">{{registrationError}}</small>
</div> </div>
<div class="actions"> <div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="register-button">Jetzt registrieren</button> <button class="button button--primary button--big actions__submit" data-cy="register-button">Konto erstellen</button>
</div>
<div class="account-link">
<p class="account-link__text">Haben Sie ein Konto?</p>
<router-link class="account-link__link text-link" :to="{name: 'login'}">Jetzt anmelden
</router-link>
</div> </div>
</form> </form>
</div> </div>
</template> </template>
<script> <script>
import REGISTRATION_MUTATION from '@/graphql/gql/mutations/registration.gql';
import {register} from '../hep-client/index'
import HELLO_EMAIL from '@/graphql/gql/local/helloEmail.gql';
function initialData() {
return {
prefix: 'Herr',
lastname: '',
firstname: '',
password: '',
passwordConfirmation: '',
street: '',
postcode: '',
city: '',
firstnameErrors: '',
lastnameErrors: '',
emailErrors: '',
passwordsErrors: [],
passwordErrors: [],
streetErrors: [],
cityErrors: [],
postcodeErrors: [],
registrationError: '',
submitted: false,
}
}
export default { export default {
components: {}, components: {},
@ -131,73 +239,78 @@ export default {
validateBeforeSubmit() { validateBeforeSubmit() {
this.$validator.validate().then(result => { this.$validator.validate().then(result => {
this.submitted = true; this.submitted = true;
let that = this;
if (result) { if (result) {
this.$apollo.mutate({ const registrationData = {
client: 'publicClient', customer: {
mutation: REGISTRATION_MUTATION, prefix: this.prefix,
variables: { email: this.helloEmail,
input: { firstname: this.firstname,
firstnameInput: this.firstname, lastname: this.lastname,
lastnameInput: this.lastname, gender: this.prefix === 'Herr' ? 1 : 2,
emailInput: this.email, addresses: [{
licenseKeyInput: this.licenseKey, street: [this.street],
} postcode: this.postcode,
city: this.city,
country_id: 'CH',
firstname: this.firstname,
lastname: this.lastname,
prefix: this.prefix,
default_shipping: true,
default_billing: true
}]
}, },
fetchPolicy: 'no-cache' password: this.password
};
register(registrationData).then((response) => {
if (response.data.id && response.data.id > 0) {
this.$router.push({name: 'checkEmail'});
}
}) })
.then(({data: {registration: { success, errors }}}) => { .catch((error) => {
try { console.warn(error);
if (success) { if (error.response.data.message && error.response.data.message === 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.') {
window.location.href = '/registration/set-password/done/'; this.emailErrors = ['Die angegebene E-Mail ist bereits registriert.'];
} else { } else {
errors.forEach(function(error) { this.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
switch (error.field) {
case 'email':
that.emailErrors = ['Die angegebene E-Mail ist bereits registriert.'];
break;
case 'license_key':
that.licenseKeyErrors = ['Die angegebenen Lizenz ist unglültig'];
}
})
}
} catch (e) {
console.warn(e);
that.registrationError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
} }
}); });
} }
}); });
}, },
resetForm() { resetForm() {
this.email = ''; Object.assign(this.$data, initialData());
this.lastname = '';
this.firstname = '';
this.licenseKey = '';
this.firstnameErrors = '';
this.lastnameErrors = '';
this.emailErrors = '';
this.licenseKeyErrors = '';
this.registrationError = '';
this.submitted = false;
this.$validator.reset(); this.$validator.reset();
} }
}, },
data() { data() {
return { return Object.assign(
email: '', {
lastname: '', helloEmail: ''
firstname: '', },
licenseKey: '', initialData()
firstnameErrors: '', );
lastnameErrors: '', },
emailErrors: '',
licenseKeyErrors: '', apollo: {
registrationError: '', helloEmail: {
submitted: false query: HELLO_EMAIL,
}; result({data}) {
} if (data.helloEmail && data.helloEmail.email === '') {
this.$router.push({name: 'hello'});
}
},
update(data) {
return data.helloEmail.email;
},
error() {
console.log('error')
this.$router.push({name: 'hello'});
}
},
},
}; };
</script> </script>

View File

@ -20,6 +20,7 @@
class="start-sections__section" class="start-sections__section"
title="Räume" title="Räume"
subtitle="Beiträge mit der Klasse teilen" subtitle="Beiträge mit der Klasse teilen"
data-cy="rooms-link"
link-text="Alle Räume anzeigen" link-text="Alle Räume anzeigen"
route="/rooms" route="/rooms"
> >

View File

@ -1,31 +0,0 @@
<template>
<div class="no-class public-page">
<h1 class="public-page__title">Sie sind keiner Klasse zugeteilt</h1>
<p>Sie können mySkillbox nur verwenden wenn Sie in einer Klasse zugeteilt sind. Aktuell kann Sie nur der mySkillbox-Support einer Klasse zuteilen.</p>
<button class="button button--primary button--big logout-button" @click="logout">Abmelden</button>
</div>
</template>
<script>
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
export default {
methods: {
logout() {
this.$apollo.mutate({
mutation: LOGOUT_MUTATION,
}).then(({data}) => {
if (data.logout.success) { location.replace('/') }
});
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.logout-button {
margin-top: $large-spacing;
}
</style>

View File

@ -28,8 +28,13 @@ import surveyPage from '@/pages/survey'
import styleGuidePage from '@/pages/styleguide' import styleGuidePage from '@/pages/styleguide'
import moduleRoom from '@/pages/moduleRoom' import moduleRoom from '@/pages/moduleRoom'
import login from '@/pages/login' import login from '@/pages/login'
import betaLogin from '@/pages/beta-login'
import hello from '@/pages/hello'
import registration from '@/pages/registration' import registration from '@/pages/registration'
import waitForClass from '@/pages/waitForClass' import checkEmail from '@/pages/check-email'
import emailVerification from '@/pages/email-verification'
import licenseActivation from '@/pages/license-activation'
import forgotPassword from '@/pages/forgot-password'
import joinClass from '@/pages/joinClass' import joinClass from '@/pages/joinClass'
import oldClasses from '@/pages/oldClasses'; import oldClasses from '@/pages/oldClasses';
import createClass from '@/pages/createClass'; import createClass from '@/pages/createClass';
@ -53,6 +58,24 @@ const routes = [
public: true public: true
} }
}, },
{
path: '/hello',
name: 'hello',
component: hello,
meta: {
layout: 'public',
public: true
}
},
{
path: '/beta-login',
name: 'betaLogin',
component: betaLogin,
meta: {
layout: 'public',
public: true
}
},
{ {
path: '/module/:slug', path: '/module/:slug',
component: moduleBase, component: moduleBase,
@ -124,7 +147,7 @@ const routes = [
{path: 'show-code', name: 'show-code', component: showCode, meta: {layout: 'simple'}}, {path: 'show-code', name: 'show-code', component: showCode, meta: {layout: 'simple'}},
] ]
}, },
{path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'simple'}}, {path: 'join-class', name: 'join-class', component: joinClass, meta: {layout: 'public'}},
{ {
path: '/survey/:id', path: '/survey/:id',
component: surveyPage, component: surveyPage,
@ -142,10 +165,39 @@ const routes = [
} }
}, },
{ {
path: '/no-class', path: '/check-email',
component: waitForClass, component: checkEmail,
name: 'noClass', name: 'checkEmail',
meta: {layout: 'public'} meta: {
public: true,
layout: 'public'
}
},
{
path: '/verify-email',
component: emailVerification,
name: 'emailVerification',
meta: {
public: true,
layout: 'public'
}
},
{
path: '/license-activation',
component: licenseActivation,
name: 'licenseActivation',
meta: {
layout: 'public'
}
},
{
path: '/forgot-password',
component: forgotPassword,
name: 'forgotPassword',
meta: {
layout: 'public',
public: true
}
}, },
{path: '/styleguide', component: styleGuidePage}, {path: '/styleguide', component: styleGuidePage},
{path: '*', component: p404} {path: '*', component: p404}

View File

@ -0,0 +1,13 @@
.info-header {
margin-top: $large-spacing;
margin-bottom: $large-spacing;
&__text {
font-family: $sans-serif-font-family;
}
&__emph {
color: $color-brand;
font-weight: 600;
}
}

View File

@ -176,3 +176,12 @@
@mixin popover-defaults() { @mixin popover-defaults() {
bottom: $popover-default-bottom; bottom: $popover-default-bottom;
} }
@mixin content-block {
padding-right: $medium-spacing;
padding-left: $medium-spacing;
max-width: 800px;
min-width: 320px;
width: 100%;
margin: 0 auto;
}

View File

@ -6,6 +6,7 @@
&__label { &__label {
margin-bottom: 10px; margin-bottom: 10px;
display: inline-block; display: inline-block;
font-weight: 600;
} }
&__input { &__input {

View File

@ -75,3 +75,8 @@ input, textarea, select, button {
.inline-title { .inline-title {
@include inline-title; @include inline-title;
} }
.hep-link {
font-family: $sans-serif-font-family;
color: $color-brand-dark;
}

View File

@ -22,6 +22,7 @@
@import "public-page"; @import "public-page";
@import "student-submission"; @import "student-submission";
@import "module-activity"; @import "module-activity";
@import "info-header";
@import "book-subnavigation"; @import "book-subnavigation";
@import "simple-list"; @import "simple-list";
@import "widget-popover"; @import "widget-popover";

View File

@ -8,5 +8,5 @@ module.exports = {
` `
} }
} }
} },
}; };

View File

@ -10,6 +10,7 @@ from assignments.schema.queries import AssignmentsQuery, StudentSubmissionQuery
from basicknowledge.queries import BasicKnowledgeQuery from basicknowledge.queries import BasicKnowledgeQuery
from books.schema.mutations.main import BookMutations from books.schema.mutations.main import BookMutations
from books.schema.queries import BookQuery from books.schema.queries import BookQuery
from core.schema.mutations.coupon import CouponMutations
from core.schema.mutations.main import CoreMutations from core.schema.mutations.main import CoreMutations
from notes.mutations import NoteMutations from notes.mutations import NoteMutations
from objectives.mutations import ObjectiveMutations from objectives.mutations import ObjectiveMutations
@ -36,7 +37,7 @@ class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQ
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, SpellCheckMutations, ProfileMutations, SurveyMutations, NoteMutations, RegistrationMutations, SpellCheckMutations,
graphene.ObjectType): CouponMutations, graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='_debug') debug = graphene.Field(DjangoDebug, name='_debug')

View File

@ -12,4 +12,16 @@ class Mutation(UserMutations, RegistrationMutations, graphene.ObjectType):
debug = graphene.Field(DjangoDebug, name='__debug') debug = graphene.Field(DjangoDebug, name='__debug')
schema = graphene.Schema(mutation=Mutation) # graphene neets some kind of schema in order to create a schema
class DummyQuery(object):
meaning_of_life = graphene.Int()
def resolve_meaning_of_life(self, info, **kwargs):
return 42
class Query(DummyQuery, graphene.ObjectType):
pass
schema = graphene.Schema(mutation=Mutation, query=Query)

View File

@ -1,21 +1,26 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.urls import include
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
from api.schema_public import schema from api.schema_public import schema
from core.views import PrivateGraphQLView from core.views import PrivateGraphQLView, ConfirmationKeyDisplayView
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))), url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())), url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())),
# hep proxy
url(r'^proxy/', include('registration.urls', namespace="registration")),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True, urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True,
pretty=True)))] pretty=True)))]
urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))] urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))]
urlpatterns += [url(r'^confirmation', ConfirmationKeyDisplayView.as_view(), name='confirmation_key_display')]

228
server/core/hep_client.py Normal file
View File

@ -0,0 +1,228 @@
from datetime import datetime, timedelta
from django.conf import settings
import logging
import requests
logger = logging.getLogger(__name__)
TEACHER_EDITION_DURATION = 365
STUDENT_EDITION_DURATION = 4*365
TEACHER_KEY = 'teacher'
STUDENT_KEY = 'student'
MYSKILLBOX_TEACHER_EDITION_ISBN = "978-3-0355-1823-8"
MYSKILLBOX_STUDENT_EDITION_ISBN = "978-3-0355-1397-4"
class HepClientException(Exception):
pass
class HepClientUnauthorizedException(Exception):
pass
class HepClient:
URL = settings.HEP_URL
WEBSITE_ID = 1
HEADERS = {
'accept': 'application/json',
'content-type': 'application/json'
}
def _call(self, url, method='get', data=None, additional_headers=None):
request_url = f'{self.URL}{url}'
if additional_headers:
headers = {**additional_headers, **self.HEADERS}
else:
headers = self.HEADERS
if method == 'post':
response = requests.post(request_url, json=data, headers=headers)
elif method == 'get':
if data:
response = requests.get(request_url, headers=headers, data=data)
else:
response = requests.get(request_url, headers=headers)
elif method == 'put':
response = requests.put(request_url, data=data)
# Todo handle 401 and most important network errors
if response.status_code == 401:
raise HepClientUnauthorizedException(response.status_code, response.json())
elif response.status_code != 200:
raise HepClientException(response.status_code, response.json())
return response
def fetch_admin_token(self, admin_user, password):
response = self._call('/rest/deutsch/V1/integration/admin/token', 'post',
data={'username': admin_user, 'password': password})
return response.content.decode('utf-8')[1:-1]
def is_email_available(self, email):
response = self._call('/rest/deutsch/V1/customers/isEmailAvailable', method='post',
data={'customerEmail': email, 'websiteId': self.WEBSITE_ID})
return response.json()
def is_email_verified(self, user_data):
return 'confirmation' not in user_data
def customer_verify_email(self, confirmation_key):
response = self._call('/rest/V1/customers/me', method='put', data={'confirmationKey': confirmation_key})
return response.json()
def customer_create(self, customer_data):
response = self._call('/rest/deutsch/V1/customers', method='post', data=customer_data)
return response.json()
def customer_token(self, username, password):
response = self._call('/rest/deutsch/V1/integration/customer/token', 'post',
data={'username': username, 'password': password})
return response.json()
def customer_me(self, token):
response = self._call('/rest/V1/customers/me', additional_headers={'authorization': f'Bearer {token}'})
return response.json()
def customer_activate(self, confirmation_key, user_id):
response = self._call(f'/customer/account/confirm/?back_url=&id={user_id}&key={confirmation_key}', method='get')
return response
def customers_search(self, admin_token, email):
response = self._call('/rest/V1/customers/search?searchCriteria[filterGroups][0][filters][0][field]='
f'email&searchCriteria[filterGroups][0][filters][0][value]={email}',
additional_headers={'authorization': f'Bearer {admin_token}'})
json_data = response.json()
if len(json_data['items']) > 0:
return json_data['items'][0]
return None
def customers_by_id(self, admin_token, user_id):
response = self._call('/rest/V1/customers/{}'.format(user_id),
additional_headers={'authorization': f'Bearer {admin_token}'})
return response.json()
def _customer_orders(self, admin_token, customer_id):
url = ('/rest/V1/orders/?searchCriteria[filterGroups][0][filters][0]['
f'field]=customer_id&searchCriteria[filterGroups][0][filters][0][value]={customer_id}')
response = self._call(url, additional_headers={'authorization': 'Bearer {}'.format(admin_token)})
return response.json()
def coupon_redeem(self, coupon, customer_id):
try:
response = self._call(f'/rest/deutsch/V1/coupon/{coupon}/customer/{customer_id}', method='put')
except HepClientException:
return None
response_data = response.json()
if response_data[0] == '201':
return None
return response_data[0]
def myskillbox_product_for_customer(self, admin_token, customer_id):
orders = self._customer_orders(admin_token, customer_id)
products = self._extract_myskillbox_products(orders)
if len(products) == 0:
return None
else:
return self._get_relevant_product(products)
def _extract_myskillbox_products(self, orders):
products = []
for order_item in orders['items']:
status = ''
if 'status' in order_item:
status = order_item['status']
for item in order_item['items']:
order_id = -1
if 'order_id' in item:
order_id = item['order_id']
if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN or \
item['sku'] == MYSKILLBOX_STUDENT_EDITION_ISBN:
product = {
'raw': item,
'activated': self._get_item_activation(order_item),
'status': status,
'order_id': order_id
}
if item['sku'] == MYSKILLBOX_TEACHER_EDITION_ISBN:
product['edition'] = TEACHER_KEY
else:
product['edition'] = STUDENT_KEY
products.append(product)
return products
def _get_item_activation(self, item):
if 'created_at' in item:
return datetime.strptime(item['created_at'], '%Y-%m-%d %H:%M:%S')
def _get_relevant_product(self, products):
def filter_valid_products(product):
if product['status'] != 'complete':
return False
if product['edition'] == TEACHER_KEY:
expiry_delta = product['activated'] + timedelta(TEACHER_EDITION_DURATION)
else:
expiry_delta = product['activated'] + timedelta(STUDENT_EDITION_DURATION)
if HepClient.is_product_active(expiry_delta, product['edition']):
return True
else:
return False
active_products = list(filter(filter_valid_products, products))
if len(active_products) == 0:
return None
elif len(active_products) == 1:
return active_products[0]
else:
return self._select_from_teacher_products(active_products)
def _select_from_teacher_products(self, active_products):
teacher_edition = None
# select first teacher product, as they are all valid it does not matter which one
for product in active_products:
if product['edition'] == TEACHER_KEY:
teacher_edition = product
break
# select a student product, as they are all valid it does not matter which one
if not teacher_edition:
return active_products[0]
return teacher_edition
@staticmethod
def is_product_active(expiry_date, edition):
if edition == TEACHER_KEY:
duration = TEACHER_EDITION_DURATION
else:
duration = STUDENT_EDITION_DURATION
now = datetime.now()
return expiry_date >= now >= expiry_date - timedelta(days=duration)

View File

@ -113,5 +113,3 @@ class Command(BaseCommand):
# now create all and rooms # now create all and rooms
management.call_command('dummy_rooms', verbosity=0) management.call_command('dummy_rooms', verbosity=0)
# create license
management.call_command('create_dummy_license', verbosity=0)

View File

@ -7,9 +7,23 @@ import os
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
filename = 'schema.json' schemas = [
{
'filename': 'schema.json',
'schema': 'api.schema.schema'
},
{
'filename': 'schema_public.json',
'schema': 'api.schema_public.schema'
}
]
for schema in schemas:
self.create_schema(schema['filename'], schema['schema'])
def create_schema(self, filename, schema):
cypress_path = os.path.join(settings.BASE_DIR, '..', 'client', 'cypress', 'fixtures', filename) cypress_path = os.path.join(settings.BASE_DIR, '..', 'client', 'cypress', 'fixtures', filename)
call_command('graphql_schema', schema='api.schema.schema', out=filename, indent=4) call_command('graphql_schema', schema=schema, out=filename, indent=4)
with open(filename) as f: with open(filename) as f:
initial_json = json.loads(f.read()) initial_json = json.loads(f.read())

View File

@ -0,0 +1,14 @@
import os
import shutil
from django.core.management import BaseCommand
from core.models import AdminData
class Command(BaseCommand):
def handle(self, *args, **options):
"Update admin token via cronjob"
AdminData.objects.update_admin_token()

30
server/core/managers.py Normal file
View File

@ -0,0 +1,30 @@
from django.conf import settings
from django.db import models
from datetime import timedelta
from django.utils import timezone
from core.hep_client import HepClient
DEFAULT_PK = 1
class AdminDataManager(models.Manager):
hep_client = HepClient()
def update_admin_token(self):
admin_token = self.hep_client.fetch_admin_token(settings.HEP_ADMIN_USER, settings.HEP_ADMIN_PASSWORD)
admin_data, created = self.get_or_create(pk=DEFAULT_PK)
admin_data.hep_admin_token = admin_token
admin_data.save()
return admin_data.hep_admin_token
def get_admin_token(self):
try:
admin_token = self.get(pk=DEFAULT_PK)
if admin_token.updated_at < timezone.now() + timedelta(hours=1):
admin_token = self.update_admin_token()
except self.model.DoesNotExist:
admin_token = self.update_admin_token()
return admin_token

View File

@ -1,10 +1,13 @@
import json
import re import re
from django.conf import settings from django.conf import settings
from django.http import Http404, HttpResponsePermanentRedirect from django.http import Http404, HttpResponsePermanentRedirect, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from core.utils import is_private_api_call_allowed
try: try:
from threading import local from threading import local
except ImportError: except ImportError:
@ -95,3 +98,13 @@ class UserLoggedInCookieMiddleWare(MiddlewareMixin):
#else if if no user and cookie remove user cookie, logout #else if if no user and cookie remove user cookie, logout
response.delete_cookie(self.cookie_name) response.delete_cookie(self.cookie_name)
return response return response
class UserHasLicenseMiddleWare(MiddlewareMixin):
def process_response(self, request, response):
if request.path == '/api/graphql/':
if not is_private_api_call_allowed(request.user, request.body):
return HttpResponse(json.dumps({'errors': ['no active license']}), status=402)
return response

View File

@ -0,0 +1,22 @@
# Generated by Django 2.0.6 on 2020-02-05 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AdminData',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hep_admin_token', models.CharField(max_length=100)),
('updated_at', models.DateTimeField(auto_now=True, null=True)),
],
),
]

View File

12
server/core/models.py Normal file
View File

@ -0,0 +1,12 @@
from datetime import datetime
from django.db import models
from core.managers import AdminDataManager
class AdminData(models.Model):
hep_admin_token = models.CharField(max_length=100, blank=False, null=False)
updated_at = models.DateTimeField(blank=False, null=True, auto_now=True)
objects = AdminDataManager()

View File

@ -0,0 +1,44 @@
import graphene
from graphene import relay
from core.hep_client import HepClient, HepClientException
from users.user_signup_login_handler import check_and_create_licenses, create_role_for_user
class Coupon(relay.ClientIDMutation):
class Input:
coupon_code = graphene.String()
success = graphene.Boolean()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
coupon_code = kwargs.get('coupon_code').strip()
hep_client = HepClient()
try:
hep_id = info.context.user.hep_id
except AttributeError:
raise Exception('not_authenticated')
try:
response = hep_client.coupon_redeem(coupon_code, hep_id)
except HepClientException:
raise Exception('unknown_error')
if not response:
raise Exception('invalid_coupon')
license, error_msg = check_and_create_licenses(hep_client, info.context.user)
# todo fail if no license
if error_msg:
raise Exception(error_msg)
create_role_for_user(info.context.user, license.for_role.key)
return cls(success=True)
class CouponMutations:
redeem_coupon = Coupon.Field()

View File

@ -7,8 +7,10 @@
# #
# Created on 22.10.18 # Created on 22.10.18
# @author: chrigu <christian.cueni@iterativ.ch> # @author: chrigu <christian.cueni@iterativ.ch>
from core.schema.mutations.coupon import Coupon
from core.schema.mutations.logout import Logout from core.schema.mutations.logout import Logout
class CoreMutations(object): class CoreMutations(object):
logout = Logout.Field() logout = Logout.Field()
coupon = Coupon.Field()

View File

@ -126,6 +126,7 @@ MIDDLEWARE += [
'core.middleware.ThreadLocalMiddleware', 'core.middleware.ThreadLocalMiddleware',
'core.middleware.CommonRedirectMiddleware', 'core.middleware.CommonRedirectMiddleware',
'core.middleware.UserLoggedInCookieMiddleWare', 'core.middleware.UserLoggedInCookieMiddleWare',
'core.middleware.UserHasLicenseMiddleWare',
] ]
ROOT_URLCONF = 'core.urls' ROOT_URLCONF = 'core.urls'
@ -380,6 +381,12 @@ EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_USE_SSL = False EMAIL_USE_SSL = False
ALLOW_BETA_LOGIN = True
# HEP
HEP_ADMIN_USER = os.environ.get("HEP_ADMIN_USER")
HEP_ADMIN_PASSWORD = os.environ.get("HEP_ADMIN_PASSWORD")
HEP_URL = os.environ.get("HEP_URL")
TASKBASE_USER = os.environ.get("TASKBASE_USER") TASKBASE_USER = os.environ.get("TASKBASE_USER")
TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD") TASKBASE_PASSWORD = os.environ.get("TASKBASE_PASSWORD")
@ -391,3 +398,4 @@ TASKBASE_BASEURL = os.environ.get("TASKBASE_BASEURL")
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_DIR = './test-reports/' TEST_OUTPUT_DIR = './test-reports/'
TEST_OUTPUT_VERBOSE = 1 TEST_OUTPUT_VERBOSE = 1

View File

@ -15,3 +15,5 @@ MIGRATION_MODULES = DisableMigrations()
SENDGRID_API_KEY = "" SENDGRID_API_KEY = ""
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
LOGIN_REDIRECT_URL = '/accounts/login/' LOGIN_REDIRECT_URL = '/accounts/login/'
USE_LOCAL_REGISTRATION = False

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Confirmation</title>
</head>
<body>
<a href="/verify-email?confirmation={{ confirmation_key }}&id={{ hep_id }}">Email bestätitgen</a>
</body>
</html>

View File

@ -0,0 +1,41 @@
{
"id": 49124,
"group_id": 1,
"default_billing": "47579",
"default_shipping": "47579",
"confirmation": "41b58ba6598a618095e8c70625d7f052",
"created_at": "2018-07-19 15:05:27",
"updated_at": "2019-11-26 17:04:29",
"created_in": "hep verlag",
"email": "1heptest19072018@mailinator.com",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"gender": 2,
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 47579,
"customer_id": 49124,
"region": {
"region_code": null,
"region": null,
"region_id": 0
},
"region_id": 0,
"country_id": "CH",
"street": [
"Test"
],
"telephone": "",
"postcode": "0000",
"city": "Test",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"default_shipping": true,
"default_billing": true
}
]
}

View File

@ -0,0 +1,40 @@
{
"id": 49124,
"group_id": 1,
"default_billing": "47579",
"default_shipping": "47579",
"created_at": "2018-07-19 15:05:27",
"updated_at": "2019-11-26 17:04:29",
"created_in": "hep verlag",
"email": "1heptest19072018@mailinator.com",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"gender": 2,
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 47579,
"customer_id": 49124,
"region": {
"region_code": null,
"region": null,
"region_id": 0
},
"region_id": 0,
"country_id": "CH",
"street": [
"Test"
],
"telephone": "",
"postcode": "0000",
"city": "Test",
"firstname": "Test",
"lastname": "Test",
"prefix": "Frau",
"default_shipping": true,
"default_billing": true
}
]
}

View File

@ -0,0 +1,526 @@
{
"items": [
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 46,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 44.88,
"base_subtotal_incl_tax": 46,
"base_tax_amount": 1.12,
"base_total_due": 46,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83693,
"created_at": "2018-07-19 15:05:33",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57612,
"global_currency_code": "CHF",
"grand_total": 46,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614768",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "71aedb",
"quote_id": 104401,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 44.88,
"subtotal_incl_tax": 46,
"tax_amount": 1.12,
"total_due": 46,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:05:33",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Myskillbox Schüler Edition",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1397-4",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83693,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57612,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 46,
"base_amount_ordered": 46,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57612,
"method": "checkmo",
"parent_id": 57612,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244885,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "licence-coupon \"ebf81a59b968\"",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244884,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:05:33",
"entity_id": 244883,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244882,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1082-9",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
]
}
]
}
},
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 24,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 23.41,
"base_subtotal_incl_tax": 24,
"base_tax_amount": 0.59,
"base_total_due": 24,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83696,
"created_at": "2018-07-19 15:19:00",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57614,
"global_currency_code": "CHF",
"grand_total": 24,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614770",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "1a88e9",
"quote_id": 104403,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 23.41,
"subtotal_incl_tax": 24,
"tax_amount": 0.59,
"total_due": 24,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:19:00",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83696,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57614,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 24,
"base_amount_ordered": 24,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57614,
"method": "checkmo",
"parent_id": 57614,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244890,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "licence-coupon \"ece5e74a2b36\"",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244889,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:19:00",
"entity_id": 244888,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244887,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
]
}
]
}
}
],
"search_criteria": {
"filter_groups": [
{
"filters": [
{
"field": "customer_id",
"value": "49124",
"condition_type": "eq"
}
]
}
]
},
"total_count": 2
}

View File

@ -0,0 +1,526 @@
{
"items": [
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 46,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 44.88,
"base_subtotal_incl_tax": 46,
"base_tax_amount": 1.12,
"base_total_due": 46,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83693,
"created_at": "2018-07-19 15:05:33",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57612,
"global_currency_code": "CHF",
"grand_total": 46,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614768",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "71aedb",
"quote_id": 104401,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 44.88,
"subtotal_incl_tax": 46,
"tax_amount": 1.12,
"total_due": 46,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:05:33",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Myskillbox Lehreredition",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1823-8",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83693,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57612,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 46,
"base_amount_ordered": 46,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57612,
"method": "checkmo",
"parent_id": 57612,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244885,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "licence-coupon \"ebf81a59b968\"",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244884,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:05:33",
"entity_id": 244883,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:05:33",
"entity_id": 244882,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57612,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 46,
"base_price": 44.88,
"base_price_incl_tax": 46,
"base_row_invoiced": 0,
"base_row_total": 44.88,
"base_row_total_incl_tax": 46,
"base_tax_amount": 1.12,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:05:33",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80317,
"name": "Gesellschaft Ausgabe A (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57612,
"original_price": 46,
"price": 44.88,
"price_incl_tax": 46,
"product_id": 8652,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135166,
"row_invoiced": 0,
"row_total": 44.88,
"row_total_incl_tax": 46,
"sku": "978-3-0355-1082-9",
"store_id": 1,
"tax_amount": 1.12,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:05:33",
"weight": 0.01
}
]
}
]
}
},
{
"base_currency_code": "CHF",
"base_discount_amount": 0,
"base_grand_total": 24,
"base_discount_tax_compensation_amount": 0,
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"base_subtotal": 23.41,
"base_subtotal_incl_tax": 24,
"base_tax_amount": 0.59,
"base_total_due": 24,
"base_to_global_rate": 1,
"base_to_order_rate": 1,
"billing_address_id": 83696,
"created_at": "2018-07-19 15:19:00",
"customer_email": "1heptest19072018@mailinator.com",
"customer_firstname": "Test",
"customer_gender": 2,
"customer_group_id": 4,
"customer_id": 49124,
"customer_is_guest": 0,
"customer_lastname": "Test",
"customer_note": "coupon",
"customer_note_notify": 1,
"customer_prefix": "Frau",
"discount_amount": 0,
"email_sent": 1,
"entity_id": 57614,
"global_currency_code": "CHF",
"grand_total": 24,
"discount_tax_compensation_amount": 0,
"increment_id": "1004614770",
"is_virtual": 1,
"order_currency_code": "CHF",
"protect_code": "1a88e9",
"quote_id": 104403,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0,
"state": "complete",
"status": "complete",
"store_currency_code": "CHF",
"store_id": 1,
"store_name": "hep verlag\nhep verlag\nhep verlag",
"store_to_base_rate": 0,
"store_to_order_rate": 0,
"subtotal": 23.41,
"subtotal_incl_tax": 24,
"tax_amount": 0.59,
"total_due": 24,
"total_item_count": 1,
"total_qty_ordered": 1,
"updated_at": "2018-07-19 15:19:00",
"weight": 0,
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
],
"billing_address": {
"address_type": "billing",
"city": "Test",
"country_id": "CH",
"customer_address_id": 47579,
"email": "1heptest19072018@mailinator.com",
"entity_id": 83696,
"firstname": "Test",
"lastname": "Test",
"parent_id": 57614,
"postcode": "0000",
"prefix": "Frau",
"street": [
"Test"
],
"telephone": null
},
"payment": {
"account_status": null,
"additional_information": [
"Rechnung",
null,
null
],
"amount_ordered": 24,
"base_amount_ordered": 24,
"base_shipping_amount": 0,
"cc_last4": null,
"entity_id": 57614,
"method": "checkmo",
"parent_id": 57614,
"shipping_amount": 0,
"extension_attributes": []
},
"status_histories": [
{
"comment": "payed by couponcode",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244890,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "licence-coupon \"ece5e74a2b36\"",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244889,
"entity_name": "order",
"is_customer_notified": null,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": null,
"created_at": "2018-07-19 15:19:00",
"entity_id": 244888,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
},
{
"comment": "Exported to ERP",
"created_at": "2018-07-19 15:19:00",
"entity_id": 244887,
"entity_name": "order",
"is_customer_notified": 0,
"is_visible_on_front": 0,
"parent_id": 57614,
"status": "complete"
}
],
"extension_attributes": {
"shipping_assignments": [
{
"shipping": {
"total": {
"base_shipping_amount": 0,
"base_shipping_discount_amount": 0,
"base_shipping_incl_tax": 0,
"base_shipping_tax_amount": 0,
"shipping_amount": 0,
"shipping_discount_amount": 0,
"shipping_discount_tax_compensation_amount": 0,
"shipping_incl_tax": 0,
"shipping_tax_amount": 0
}
},
"items": [
{
"amount_refunded": 0,
"base_amount_refunded": 0,
"base_discount_amount": 0,
"base_discount_invoiced": 0,
"base_discount_tax_compensation_amount": 0,
"base_original_price": 24,
"base_price": 23.41,
"base_price_incl_tax": 24,
"base_row_invoiced": 0,
"base_row_total": 23.41,
"base_row_total_incl_tax": 24,
"base_tax_amount": 0.59,
"base_tax_invoiced": 0,
"created_at": "2018-07-19 15:19:00",
"discount_amount": 0,
"discount_invoiced": 0,
"discount_percent": 0,
"free_shipping": 0,
"discount_tax_compensation_amount": 0,
"is_qty_decimal": 0,
"is_virtual": 1,
"item_id": 80320,
"name": "Gesellschaft Ausgabe A, Arbeitsheft (eLehrmittel, Neuauflage)",
"no_discount": 0,
"order_id": 57614,
"original_price": 24,
"price": 23.41,
"price_incl_tax": 24,
"product_id": 8654,
"product_type": "virtual",
"qty_canceled": 0,
"qty_invoiced": 0,
"qty_ordered": 1,
"qty_refunded": 0,
"qty_shipped": 0,
"quote_item_id": 135169,
"row_invoiced": 0,
"row_total": 23.41,
"row_total_incl_tax": 24,
"sku": "978-3-0355-1185-7",
"store_id": 1,
"tax_amount": 0.59,
"tax_invoiced": 0,
"tax_percent": 2.5,
"updated_at": "2018-07-19 15:19:00",
"weight": 0.01
}
]
}
]
}
}
],
"search_criteria": {
"filter_groups": [
{
"filters": [
{
"field": "customer_id",
"value": "49124",
"condition_type": "eq"
}
]
}
]
},
"total_count": 2
}

View File

@ -0,0 +1,47 @@
import json
import os
from datetime import datetime, timedelta
class MockResponse:
def __init__(self, status_code, data={}):
self.status_code = status_code
self.data = data
def json(self):
return self.data
## Setup json data
def make_orders_valid(order_items):
for order_item in order_items['items']:
if 'created_at' in order_item:
yesterday = datetime.now() - timedelta(1)
order_item['created_at'] = datetime.strftime(yesterday, '%Y-%m-%d %H:%M:%S')
return order_items
# Load data
dir_path = os.path.dirname(os.path.realpath(__file__))
with open('{}/mock_data/valid_teacher_orders.json'.format(dir_path), 'r') as file:
valid_teacher_order_data = file.read()
with open('{}/mock_data/valid_student_orders.json'.format(dir_path), 'r') as file:
valid_student_order_data = file.read()
with open('{}/mock_data/me_data.json'.format(dir_path), 'r') as file:
me_data = file.read()
with open('{}/mock_data/email_not_confirmed_me.json'.format(dir_path), 'r') as file:
not_confirmed_email_me_data = file.read()
ME_DATA = json.loads(me_data)
NOT_CONFIRMED_ME = json.loads(not_confirmed_email_me_data)
valid_teacher_order_items = json.loads(valid_teacher_order_data)
VALID_TEACHERS_ORDERS = make_orders_valid(valid_teacher_order_items)
valid_student_order_items = json.loads(valid_student_order_data)
VALID_STUDENT_ORDERS = make_orders_valid(valid_student_order_items)

View File

@ -20,7 +20,7 @@ class ApiAccessTestCase(TestCase):
self.assertEqual(response.url, '/login?next=/api/graphql/') self.assertEqual(response.url, '/login?next=/api/graphql/')
def test_graphqlEndpoint_shouldBeAccessibleWithLogin(self): def test_graphqlEndpoint_shouldBeAccessibleWithLogin(self):
user = UserFactory(username='admin') UserFactory(username='admin')
c = Client() c = Client()
c.login(username='admin', password='test') c.login(username='admin', password='test')
@ -31,14 +31,11 @@ class ApiAccessTestCase(TestCase):
def test_publicGraphqlEndpoint_shouldBeAccessibleWithoutLogin(self): def test_publicGraphqlEndpoint_shouldBeAccessibleWithoutLogin(self):
query= json.dumps({ query= json.dumps({
'operationName': 'Login', 'operationName': 'BetaLogin',
'query': ''' 'query': '''
mutation Login($input: LoginInput!){ mutation BetaLogin($input: BetaLoginInput!){
login(input: $input) { betaLogin(input: $input) {
success success
errors {
field
}
} }
} }
''', ''',

View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 03.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
from unittest.mock import patch
import requests
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema import schema
from core.factories import UserFactory
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import MockResponse, VALID_TEACHERS_ORDERS
from users.models import License, Role, SchoolClass, UserRole
class CouponTests(TestCase):
def setUp(self):
Role.objects.create_default_roles()
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch', hep_id=3)
Role.objects.create_default_roles()
self.teacher_role = Role.objects.get_default_teacher_role()
# adding session
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.user = self.user
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_coupon_mutation(self, coupon_code, client):
mutation = '''
mutation Coupon($input: CouponInput!){
coupon(input: $input) {
success
}
}
'''
return client.execute(mutation, variables={
'input': {
'couponCode': coupon_code
}
})
@patch.object(requests, 'put', return_value=MockResponse(200, data=['200', 'Coupon successfully redeemed']))
@patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(HepClient, 'fetch_admin_token', return_value={'token': 'AABBCCDDEE**44566'})
def test_user_has_valid_coupon(self, admin_mock, orders_mock, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
user_role_key = self.user.user_roles.get(user=self.user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=self.user)
self.assertIsNotNone(license)
school_class = SchoolClass.objects.get(users__in=[self.user])
self.assertIsNotNone(school_class)
self.assertTrue(result.get('data').get('coupon').get('success'))
self.assertTrue(self.user.is_authenticated)
@patch.object(requests, 'put', return_value=MockResponse(200, data=['201', 'Invalid Coupon']))
def test_user_has_invalid_coupon(self, response_mock):
result = self.make_coupon_mutation('COUPON--1234', self.client)
self.assertEqual(result.get('errors')[0].get('message'), 'invalid_coupon')
@patch.object(requests, 'put', return_value=MockResponse(200, data=['201', 'Invalid Coupon']))
def test_unauthenticated_user_cannot_redeem(self, response_mock):
request = RequestFactory().post('/')
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
client = Client(schema=schema, context_value=request)
result = self.make_coupon_mutation('COUPON--1234', client)
self.assertEqual(result.get('errors')[0].get('message'), 'not_authenticated')

View File

@ -0,0 +1,148 @@
import json
from datetime import datetime, timedelta
from django.test import TestCase
from core.hep_client import HepClient, TEACHER_EDITION_DURATION
class HepClientTestCases(TestCase):
def setUp(self):
self.hep_client = HepClient()
self.now = datetime.now()
def test_has_no_valid_product(self):
products = [
{
'edition': 'teacher',
'raw': {},
'activated': self.now - timedelta(2*TEACHER_EDITION_DURATION),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {},
'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {},
'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION),
'status': 'complete'
}
]
relevant_product = self.hep_client._get_relevant_product(products)
self.assertIsNone(relevant_product)
def test_has_no_not_completed_product(self):
products = [
{
'edition': 'teacher',
'raw': {},
'activated': self.now - timedelta(7),
'status': 'not'
}
]
relevant_product = self.hep_client._get_relevant_product(products)
self.assertIsNone(relevant_product)
def test_has_valid_product(self):
products = [
{
'edition': 'teacher',
'raw': {
'id': 0
},
'activated': self.now - timedelta(7),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {
'id': 1
},
'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {
'id': 2
},
'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION),
'status': 'complete'
}
]
relevant_product = self.hep_client._get_relevant_product(products)
self.assertEqual(relevant_product['raw']['id'], 0)
def test_has_multiple_valid_teacher_products_but_only_one_active(self):
products = [
{
'edition': 'teacher',
'raw': {
'id': 0
},
'activated': self.now - timedelta(7),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {
'id': 1
},
'activated': self.now - timedelta(3 * TEACHER_EDITION_DURATION),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {
'id': 2
},
'activated': self.now - timedelta(4 * TEACHER_EDITION_DURATION),
'status': 'complete'
}
]
relevant_product = self.hep_client._get_relevant_product(products)
self.assertEqual(relevant_product['raw']['id'], 0)
def test_has_valid_student_and_teacher_edition(self):
products = [
{
'edition': 'student',
'raw': {
'id': 0
},
'activated': self.now - timedelta(7),
'status': 'complete'
},
{
'edition': 'teacher',
'raw': {
'id': 1
},
'activated': self.now - timedelta(7),
'status': 'complete'
}
]
relevant_product = self.hep_client._select_from_teacher_products(products)
self.assertEqual(relevant_product['raw']['id'], 1)
def test_product_is_active(self):
expiry_date = self.now + timedelta(3)
is_active = HepClient.is_product_active(expiry_date, 'teacher')
self.assertTrue(is_active)
def test_product_is_not_active(self):
expiry_date = self.now - timedelta(3 * TEACHER_EDITION_DURATION)
is_active = HepClient.is_product_active(expiry_date, 'teacher')
self.assertFalse(is_active)

View File

@ -1,4 +1,4 @@
from django.test import TestCase, Client from django.test import TestCase
from django.core import management from django.core import management
from users.models import User, Role from users.models import User, Role

View File

@ -0,0 +1,49 @@
from datetime import timedelta
from django.test import TestCase
from django.utils import timezone
from core.factories import UserFactory
from core.utils import is_private_api_call_allowed
class MiddlewareTestCase(TestCase):
def test_user_with_license_can_see_private_api(self):
tomorrow = timezone.now() + timedelta(1)
user = UserFactory(username='aschiman@ch.ch')
user.license_expiry_date = tomorrow
body = b'"{mutation {\\n addRoom}"'
self.assertTrue(is_private_api_call_allowed(user, body))
def test_user_without_valid_license_cannot_see_private_api(self):
yesterday = timezone.now() - timedelta(1)
user = UserFactory(username='aschiman@ch.ch', hep_id=23)
user.license_expiry_date = yesterday
body = b'"{mutation {\\n addRoom}"'
self.assertFalse(is_private_api_call_allowed(user, body))
def test_logout_is_allowed_without_valid_license(self):
yesterday = timezone.now() - timedelta(1)
user = UserFactory(username='aschiman@ch.ch')
user.license_expiry_date = yesterday
body = b'"{mutation { logout {"'
self.assertTrue(is_private_api_call_allowed(user, body))
def test_me_query_is_allowed_without_valid_license(self):
yesterday = timezone.now() - timedelta(1)
user = UserFactory(username='aschiman@ch.ch')
user.license_expiry_date = yesterday
body = b'"{query { me {"'
self.assertTrue(is_private_api_call_allowed(user, body))

View File

@ -2,41 +2,19 @@ from django.conf import settings
from django.conf.urls import url, include from django.conf.urls import url, include
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import PasswordResetView from django.urls import re_path
from django.urls import re_path, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
from wagtail.admin import urls as wagtailadmin_urls from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls from wagtail.core import urls as wagtail_urls
from core import views from core import views
from core.views import LegacySetPasswordView, LegacySetPasswordDoneView, LegacySetPasswordConfirmView,\
LegacySetPasswordCompleteView, SetPasswordView, SetPasswordDoneView, SetPasswordConfirmView, SetPasswordCompleteView
urlpatterns = [ urlpatterns = [
# django admin # django admin
url(r'^guru/', admin.site.urls), url(r'^guru/', admin.site.urls),
url(r'^statistics/', include('statistics.urls', namespace='statistics')), url(r'^statistics/', include('statistics.urls', namespace='statistics')),
# legacy - will be removed
# forgot password
path('accounts/password_reset/',
PasswordResetView.as_view(html_email_template_name='registration/password_reset_email.html')),
path(r'accounts/', include('django.contrib.auth.urls')),
# set password
path('welcome/', LegacySetPasswordView.as_view(), name='set_password'),
path('set-password/done/', LegacySetPasswordDoneView.as_view(), name='set_password_done'),
path('set-password/<uidb64>/<token>/', LegacySetPasswordConfirmView.as_view(), name='set_password_confirm'),
path('set-password/complete/', LegacySetPasswordCompleteView.as_view(), name='set_password_complete'),
# set password upon registration
path('registration/welcome/', SetPasswordView.as_view(), name='registration_set_password'),
path('registration/set-password/done/', SetPasswordDoneView.as_view(), name='registration_set_password_done'),
path('registration/set-password/<uidb64>/<token>/', SetPasswordConfirmView.as_view(),
name='registration_set_password_confirm'),
path('registration/set-password/complete/', SetPasswordCompleteView.as_view(),
name='registration_set_password_complete'),
# wagtail # wagtail
url(r'^cms/', include(wagtailadmin_urls)), url(r'^cms/', include(wagtailadmin_urls)),

View File

@ -1,3 +1,7 @@
import re
from django.utils import timezone
from api.utils import get_object from api.utils import get_object
from users.models import SchoolClass from users.models import SchoolClass
@ -18,3 +22,29 @@ def set_visible_for(block, visibility_list):
block.visible_for.remove(school_class) block.visible_for.remove(school_class)
else: else:
block.visible_for.add(school_class) block.visible_for.add(school_class)
def is_private_api_call_allowed(user, body):
# logged in users should only be able to access all resources if they have a valid license
# logged in users without valid license have only access to logout, me & coupon mutations
body_unicode = body.decode('utf-8')
try:
if not user.hep_id:
return True
except AttributeError:
return True
# logout, me and coupon resources are always allowed. Even if the user has no valid license
if re.search(r"mutation\s*.*\s*logout\s*{", body_unicode) or re.search(r"query\s*.*\s*me\s*{", body_unicode)\
or re.search(r"mutation\s*Coupon", body_unicode):
return True
license_expiry = user.license_expiry_date
# all other resources are denied if the license is not valid
if license_expiry is None or license_expiry < timezone.now():
return False
return True

View File

@ -7,9 +7,13 @@ from django.http.response import HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import TemplateView
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.hep_client import HepClient
from core.models import AdminData
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
pass pass
@ -26,50 +30,18 @@ def home(request):
return render(request, 'index.html', {}) return render(request, 'index.html', {})
class SetPasswordView(PasswordResetView): class ConfirmationKeyDisplayView(TemplateView):
html_email_template_name = 'registration/registration_set_password_email.html' template_name = 'confirmation_key.html'
subject_template_name = 'registration/registration_set_password_subject.txt'
success_url = reverse_lazy('registration_set_password_done')
template_name = 'registration/registration_set_password_form.html'
title = _('Password setzen')
def get_context_data(self, *args, **kwargs):
class SetPasswordDoneView(PasswordResetDoneView): email = self.request.GET.get('email', '')
template_name = 'registration/registration_set_password_done.html'
title = _('Password setzen versandt')
hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
hep_user = hep_client.customers_search(admin_token, email)
class SetPasswordConfirmView(PasswordResetConfirmView): context = super().get_context_data(**kwargs)
success_url = reverse_lazy('registration_set_password_complete') context['confirmation_key'] = hep_user['confirmation']
template_name = 'registration/registration_set_password_confirm.html' context['hep_id'] = hep_user['id']
title = _('Gib ein Passwort ein') return context
class SetPasswordCompleteView(PasswordResetCompleteView):
template_name = 'registration/registration_set_password_complete.html'
title = _('Passwort setzen erfolgreich')
# legacy
class LegacySetPasswordView(PasswordResetView):
html_email_template_name = 'registration/set_password_email.html'
subject_template_name = 'registration/set_password_subject.txt'
success_url = reverse_lazy('set_password_done')
template_name = 'registration/set_password_form.html'
title = _('Password setzen')
class LegacySetPasswordDoneView(PasswordResetDoneView):
template_name = 'registration/set_password_done.html'
title = _('Password setzen versandt')
class LegacySetPasswordConfirmView(PasswordResetConfirmView):
success_url = reverse_lazy('set_password_complete')
template_name = 'registration/set_password_confirm.html'
title = _('Gib ein Passwort ein')
class LegacySetPasswordCompleteView(PasswordResetCompleteView):
template_name = 'registration/set_password_complete.html'
title = _('Passwort setzen erfolgreich')

View File

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-10
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib import admin
from registration.models import LicenseType, License
@admin.register(LicenseType)
class LicenseTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'key', 'for_role', 'active')
list_filter = ('for_role', 'active')
@admin.register(License)
class LicenseAdmin(admin.ModelAdmin):
list_display = ('license_type', 'licensee')
list_filter = ('license_type', 'licensee')
raw_id_fields = ('licensee',)

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
import random
import factory
from registration.models import LicenseType, License
class LicenseTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = LicenseType
name = factory.Sequence(lambda n: 'license-{}'.format(n))
active = True
key = factory.Sequence(lambda n: "license-key-%03d" % n)
description = factory.Sequence(lambda n: "Some description %03d" % n)
class LicenseFactory(factory.django.DjangoModelFactory):
class Meta:
model = License

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-23
# @author: chrigu <christian.cueni@iterativ.ch>
from django.conf import settings
from django.core.management import BaseCommand
from registration.models import LicenseType
from users.models import Role
class Command(BaseCommand):
def handle(self, *args, **options):
try:
role = Role.objects.get(key=Role.objects.TEACHER_KEY)
except Role.DoesNotExist:
print("LicenseType requires that a Teacher Role exsits")
LicenseType.objects.create(name='dummy_license',
for_role=role,
active=True,
key='c1fa2e2a-2e27-480d-8469-2e88414c4ad8',
description='dummy license')

View File

@ -0,0 +1,41 @@
# Generated by Django 2.0.6 on 2020-02-04 13:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0010_schoolclass_code'),
('registration', '0002_auto_20191010_0905'),
]
operations = [
migrations.RemoveField(
model_name='licensetype',
name='for_role',
),
migrations.RemoveField(
model_name='license',
name='license_type',
),
migrations.AddField(
model_name='license',
name='expire_date',
field=models.DateField(null=True),
),
migrations.AddField(
model_name='license',
name='for_role',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.Role'),
),
migrations.AddField(
model_name='license',
name='raw',
field=models.TextField(default=''),
),
migrations.DeleteModel(
name='LicenseType',
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.1.15 on 2020-02-20 10:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('registration', '0003_auto_20200204_1331'),
]
operations = [
migrations.RemoveField(
model_name='license',
name='for_role',
),
migrations.RemoveField(
model_name='license',
name='licensee',
),
migrations.DeleteModel(
name='License',
),
]

View File

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from django.utils.translation import ugettext_lazy as _
from django.db import models
from users.managers import RoleManager
from users.models import Role, User
class LicenseType(models.Model):
name = models.CharField(_('License name'), max_length=255, blank=False, null=False)
for_role = models.ForeignKey(Role, blank=False, null=False, on_delete=models.CASCADE)
key = models.CharField(max_length=128, blank=False, null=False, unique=True)
active = models.BooleanField(_('License active'), default=False)
description = models.TextField(_('Description'), default="")
def is_teacher_license(self):
return self.for_role.key == RoleManager.TEACHER_KEY
def __str__(self):
return '%s - role: %s' % (self.name, self.for_role)
class License(models.Model):
license_type = models.ForeignKey(LicenseType, blank=False, null=False, on_delete=models.CASCADE)
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)

View File

@ -8,87 +8,60 @@
# Created on 2019-10-08 # Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch> # @author: chrigu <christian.cueni@iterativ.ch>
import graphene import graphene
from django.contrib.auth import login
from graphene import relay from graphene import relay
from core.views import SetPasswordView from core.hep_client import HepClient, HepClientException
from registration.models import License from core.models import AdminData
from registration.serializers import RegistrationSerializer from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, NO_VALID_LICENSE
from users.models import User, Role, UserRole, SchoolClass, SchoolClassMember
class PublicFieldError(graphene.ObjectType):
code = graphene.String()
class MutationError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(PublicFieldError)
class Registration(relay.ClientIDMutation): class Registration(relay.ClientIDMutation):
class Input: class Input:
firstname_input = graphene.String() confirmation_key = graphene.String()
lastname_input = graphene.String() user_id = graphene.Int()
email_input = graphene.String()
license_key_input = graphene.String()
success = graphene.Boolean() success = graphene.Boolean()
errors = graphene.List(MutationError) # todo: change for consistency message = graphene.String()
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
first_name = kwargs.get('firstname_input') confirmation_key = kwargs.get('confirmation_key')
last_name = kwargs.get('lastname_input') user_id = kwargs.get('user_id')
email = kwargs.get('email_input')
license_key = kwargs.get('license_key_input')
registration_data = {
'first_name': first_name,
'last_name': last_name,
'email': email,
'license_key': license_key,
}
serializer = RegistrationSerializer(data=registration_data) hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
if serializer.is_valid(): try:
user = User.objects.create_user_with_random_password(serializer.data['first_name'], hep_client.customer_activate(confirmation_key, user_id)
serializer.data['last_name'], user_data = hep_client.customers_by_id(admin_token, user_id)
serializer.data['email']) # double check if user has verified his email. If the "confirmation" field is present, the email address
sb_license = License.objects.create(licensee=user, license_type=serializer.context['license_type']) # is not verified.
if 'confirmation' in user_data:
return cls.return_fail_registration_msg('invalid_key')
except HepClientException:
return cls.return_fail_registration_msg('unknown_error')
if sb_license.license_type.is_teacher_license(): user, status_msg = handle_user_and_verify_products(user_data)
teacher_role = Role.objects.get(key=Role.objects.TEACHER_KEY)
UserRole.objects.get_or_create(user=user, role=teacher_role) if user:
default_class_name = SchoolClass.generate_default_group_name(user=user) login(info.context, user)
default_class = SchoolClass.objects.create(name=default_class_name)
SchoolClassMember.objects.create( if status_msg:
user=user, if status_msg == NO_VALID_LICENSE:
school_class=default_class return cls(success=True, message=NO_VALID_LICENSE)
)
else: else:
student_role = Role.objects.get(key=Role.objects.STUDENT_KEY) return cls.return_fail_registration_msg(status_msg)
UserRole.objects.get_or_create(user=user, role=student_role)
password_reset_view = SetPasswordView() return cls(success=True, message='success')
password_reset_view.request = info.context
form = password_reset_view.form_class({'email': user.email})
if not form.is_valid(): @classmethod
return cls(success=False, errors=form.errors) def return_fail_registration_msg(cls, message):
if message == UNKNOWN_ERROR:
raise Exception(message)
password_reset_view.form_valid(form) return cls(success=False, message=message)
return cls(success=True)
errors = []
for key, value in serializer.errors.items():
error = MutationError(field=key, errors=[])
for field_error in serializer.errors[key]:
error.errors.append(PublicFieldError(code=field_error.code))
errors.append(error)
return cls(success=False, errors=errors)
class RegistrationMutations: class RegistrationMutations:

View File

@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.fields import CharField, EmailField
from django.utils.translation import ugettext_lazy as _
from registration.models import License, LicenseType
class RegistrationSerializer(serializers.Serializer):
first_name = CharField(allow_blank=False)
last_name = CharField(allow_blank=False)
email = EmailField(allow_blank=False)
license_key = CharField(allow_blank=False)
skillbox_license = None
def validate_email(self, value):
lower_email = value.lower()
# the email is used as username
if len(get_user_model().objects.filter(username=lower_email)) > 0:
raise serializers.ValidationError(_(u'Diese E-Mail ist bereits registriert'))
elif len(get_user_model().objects.filter(email=lower_email)) > 0:
raise serializers.ValidationError(_(u'Dieser E-Mail ist bereits registriert'))
else:
return lower_email
def validate_license_key(self, value):
license_types = LicenseType.objects.filter(key=value, active=True)
if len(license_types) == 0:
raise serializers.ValidationError(_(u'Die Lizenznummer ist ungültig'))
self.context['license_type'] = license_types[0] # Assuming there is just ONE license per key
return value

View File

@ -7,32 +7,29 @@
# #
# Created on 2019-10-08 # Created on 2019-10-08
# @author: chrigu <christian.cueni@iterativ.ch> # @author: chrigu <christian.cueni@iterativ.ch>
from django.core import mail from unittest.mock import patch
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from graphene.test import Client from graphene.test import Client
from api.schema import schema from api.schema import schema
from registration.factories import LicenseTypeFactory, LicenseFactory from core.hep_client import HepClient
from registration.models import License from core.tests.mock_hep_data_factory import ME_DATA, VALID_TEACHERS_ORDERS
from users.managers import RoleManager from users.models import License
from users.models import Role, User, UserRole, SchoolClass from users.models import User, Role, SchoolClass
INVALID_KEY_ME = dict(ME_DATA)
INVALID_KEY_ME['confirmation'] = 'abddddddd'
class RegistrationTests(TestCase): class RegistrationTests(TestCase):
def setUp(self): def setUp(self):
self.teacher_role = Role.objects.create(key=Role.objects.TEACHER_KEY, name="Teacher Role")
self.student_role = Role.objects.create(key=Role.objects.STUDENT_KEY, name="Student Role")
self.teacher_license_type = LicenseTypeFactory(for_role=self.teacher_role)
self.student_license_type = LicenseTypeFactory(for_role=self.student_role)
self.teacher_license = LicenseFactory(license_type=self.teacher_license_type)
self.student_license = LicenseFactory(license_type=self.student_license_type)
request = RequestFactory().post('/') request = RequestFactory().post('/')
Role.objects.create_default_roles()
self.email = 'sepp@skillbox.iterativ.ch' self.email = 'sepp@skillbox.iterativ.ch'
self.first_name = 'Sepp' self.first_name = 'Sepp'
self.last_name = 'Feuz' self.last_name = 'Feuz'
@ -43,78 +40,62 @@ class RegistrationTests(TestCase):
request.session.save() request.session.save()
self.client = Client(schema=schema, context_value=request) self.client = Client(schema=schema, context_value=request)
def make_register_mutation(self, first_name, last_name, email, license_key): def make_register_mutation(self, confirmation_key, user_id):
mutation = ''' mutation = '''
mutation Registration($input: RegistrationInput!){ mutation Registration($input: RegistrationInput!){
registration(input: $input) { registration(input: $input) {
success success
errors { message
field
}
} }
} }
''' '''
return self.client.execute(mutation, variables={ return self.client.execute(mutation, variables={
'input': { 'input': {
'firstnameInput': first_name, 'confirmationKey': confirmation_key,
'lastnameInput': last_name, 'userId': user_id
'emailInput': email,
'licenseKeyInput': license_key,
} }
}) })
def _assert_user_registration(self, count, email, role_key): @patch.object(HepClient, 'customer_activate', return_value="Response")
users = User.objects.filter(username=self.email) @patch.object(HepClient, 'customers_by_id', return_value=ME_DATA)
self.assertEqual(len(users), count) @patch.object(HepClient, 'myskillbox_product_for_customer', return_value=None)
user_roles = UserRole.objects.filter(user__email=email, role__key=role_key) @patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
self.assertEqual(len(user_roles), count) def test_user_can_register_with_valid_confirmation_key_and_no_license(self, admin_mock, customer_by_id_mock,
licenses = License.objects.filter(licensee__email=email, license_type__for_role__key=role_key) product_mock, customer_mock):
self.assertEqual(len(licenses), count)
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
def test_user_can_register_as_teacher(self):
self._assert_user_registration(0, self.email, RoleManager.TEACHER_KEY)
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
self.assertEqual(len(school_classes), 0)
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.teacher_license_type.key)
self.assertTrue(result.get('data').get('registration').get('success')) self.assertTrue(result.get('data').get('registration').get('success'))
self._assert_user_registration(1, self.email, RoleManager.TEACHER_KEY) self.assertEqual(result.get('data').get('registration').get('message'), 'no_valid_license')
school_classes = SchoolClass.objects.filter(name__startswith='Meine Klasse')
self.assertEqual(len(school_classes), 1) @patch.object(HepClient, 'customer_activate', return_value="Response")
user = User.objects.get(email=self.email) @patch.object(HepClient, 'customers_by_id', return_value=INVALID_KEY_ME)
self.assertTrue(school_classes[0].is_user_in_schoolclass(user)) @patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
self.assertEqual(len(mail.outbox), 1) def test_user_cannot_register_with_invalid_key(self, admin_mock, confirmation_mock, id_mock):
self.assertEqual(mail.outbox[0].subject, 'Myskillbox: E-Mail bestätigen und Passwort setzen')
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
self.assertFalse(result.get('data').get('registration').get('success'))
self.assertEqual(result.get('data').get('registration').get('message'), 'invalid_key')
@patch.object(HepClient, '_customer_orders', return_value=VALID_TEACHERS_ORDERS)
@patch.object(HepClient, 'customer_activate', return_value="Response")
@patch.object(HepClient, 'customers_by_id', return_value=ME_DATA)
@patch.object(HepClient, 'fetch_admin_token', return_value=b'"AABBCCDDEE**44566"')
def test_teacher_can_register_with_remote_license(self, admin_mock, id_mock, activate_mock, orders_mock):
result = self.make_register_mutation('CONFIRMATION_KEY', 1)
user = User.objects.get(email=ME_DATA['email'])
user_role_key = user.user_roles.get(user=user).role.key
self.assertEqual(user_role_key, Role.objects.TEACHER_KEY)
license = License.objects.get(licensee=user)
self.assertEqual(license.for_role.key, Role.objects.TEACHER_KEY)
school_class = SchoolClass.objects.get(users__in=[user])
self.assertIsNotNone(school_class)
def test_user_can_register_as_student(self):
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
self.assertTrue(result.get('data').get('registration').get('success')) self.assertTrue(result.get('data').get('registration').get('success'))
self._assert_user_registration(1, self.email, RoleManager.STUDENT_KEY) self.assertTrue(user.is_authenticated)
def test_existing_user_cannot_register(self):
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
def test_existing_user_cannot_register_with_uppercase_email(self):
self._assert_user_registration(0, self.email, RoleManager.STUDENT_KEY)
self.make_register_mutation(self.first_name, self.last_name, self.email.upper(), self.student_license_type.key)
result = self.make_register_mutation(self.first_name, self.last_name, self.email, self.student_license_type.key)
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
def test_user_cannot_register_if_firstname_is_missing(self):
result = self.make_register_mutation('', self.last_name, self.email, self.teacher_license_type.key)
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'first_name')
self.assertFalse(result.get('data').get('registration').get('success'))
def test_user_cannot_register_if_lastname_is_missing(self):
result = self.make_register_mutation(self.first_name, '', self.email, self.teacher_license_type.key)
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'last_name')
self.assertFalse(result.get('data').get('registration').get('success'))
def test_user_cannot_register_if_email_is_missing(self):
result = self.make_register_mutation(self.first_name, self.last_name, '', self.teacher_license_type.key)
self.assertEqual(result.get('data').get('registration').get('errors')[0].get('field'), 'email')
self.assertFalse(result.get('data').get('registration').get('success'))

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 25.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
import json
from unittest.mock import patch
import requests
from django.test import TestCase, Client
from django.urls import reverse
from core.hep_client import HepClient
from core.tests.mock_hep_data_factory import MockResponse
RESPONSE = {
'id': 1234,
'confirmation': 'abdc1234',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'email': 'aschima@ch.ch',
'prefix': 'Herr',
'gender': 1,
'addresses': [
{
'country_id': 'CH',
'street': ['Weg 1'],
'postcode': '1234',
'city': 'Äussere Einöde',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'prefix': 'Herr',
'default_shipping': True,
'default_billing': True,
}
],
}
DATA = {
'customer': {
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'email': 'aschima@ch.ch',
'prefix': 'Herr',
'gender': 1,
'addresses': [
{
'country_id': 'CH',
'street': ['Weg 1'],
'postcode': '1234',
'city': 'Äussere Einöde',
'firstname': 'Pesche',
'lastname': 'Zubrüti',
'prefix': 'Herr',
'default_shipping': True,
'default_billing': True,
}
],
'password': '123454abasfd'
}
}
class ProxyTest(TestCase):
def setUp(self):
self.client = Client()
@patch.object(HepClient, 'customer_create', return_value=RESPONSE)
def test_proxy_filters_confirmation_key(self, create_mock):
response = self.client.post(reverse('api:registration:proxy'), json.dumps(DATA), content_type="application/json")
found = 'confirmation' in response.json().keys()
self.assertFalse(found)
@patch.object(requests, 'post', return_value=MockResponse(400,
data={'message': 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.'}))
def test_handles_400(self, create_mock):
response = self.client.post(reverse('api:registration:proxy'), json.dumps(DATA), content_type="application/json")
self.assertEquals(response.json()['message'], 'Ein Kunde mit der gleichen E-Mail-Adresse existiert bereits in einer zugeordneten Website.')

View File

@ -0,0 +1,9 @@
from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt
from registration.view import RegistrationProxyView
app_name = 'registration'
urlpatterns = [
url(r'^registration/', csrf_exempt(RegistrationProxyView.as_view()), name="proxy"),
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2020 ITerativ GmbH. All rights reserved.
#
# Created on 25.02.20
# @author: chrigu <christian.cueni@iterativ.ch>
import json
from django.http import JsonResponse
from django.views import View
from core.hep_client import HepClient, HepClientException
class RegistrationProxyView(View):
def post(self, request, *args, **kwargs):
hep_client = HepClient()
data = json.loads(request.body)
data['customer']['group_id'] = 5
try:
hep_data = hep_client.customer_create(data)
except HepClientException as e:
return JsonResponse(e.args[1], status=e.args[0])
response_data = hep_data.copy()
del response_data['confirmation']
return JsonResponse(response_data)

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from users.forms import CustomUserCreationForm, CustomUserChangeForm from users.forms import CustomUserCreationForm, CustomUserChangeForm
from .models import User, SchoolClass, Role, UserRole, UserSetting from .models import User, SchoolClass, Role, UserRole, UserSetting, License
class SchoolClassInline(admin.TabularInline): class SchoolClassInline(admin.TabularInline):
@ -68,3 +68,10 @@ admin.site.register(User, CustomUserAdmin)
class UserSettingAdmin(admin.ModelAdmin): class UserSettingAdmin(admin.ModelAdmin):
list_display = ('user', 'selected_class') list_display = ('user', 'selected_class')
raw_id_fields = ('user', 'selected_class') raw_id_fields = ('user', 'selected_class')
@admin.register(License)
class LicenseAdmin(admin.ModelAdmin):
list_display = ('licensee',)
list_filter = ('licensee',)
raw_id_fields = ('licensee',)

View File

@ -1,8 +1,7 @@
import random import random
import factory import factory
from users.models import SchoolClass, SchoolClassMember, License
from users.models import SchoolClass, SchoolClassMember
class_types = ['DA', 'KV', 'INF', 'EE'] class_types = ['DA', 'KV', 'INF', 'EE']
class_suffix = ['A', 'B', 'C', 'D', 'E'] class_suffix = ['A', 'B', 'C', 'D', 'E']
@ -30,3 +29,8 @@ class SchoolClassFactory(factory.django.DjangoModelFactory):
# A list of groups were passed in, use them # A list of groups were passed in, use them
for user in extracted: for user in extracted:
SchoolClassMember.objects.create(user=user, school_class=self, active=True) SchoolClassMember.objects.create(user=user, school_class=self, active=True)
class LicenseFactory(factory.django.DjangoModelFactory):
class Meta:
model = License

View File

@ -0,0 +1 @@
from django.conf import settings

View File

@ -0,0 +1 @@
from django.conf import settings

View File

@ -0,0 +1,31 @@
import os
import shutil
from django.conf import settings
from django.core.management import BaseCommand
from core.hep_client import HepClient
from core.models import AdminData
from users.models import User, License
class Command(BaseCommand):
def handle(self, *args, **options):
"Update licenses via cronjob"
hep_client = HepClient()
admin_token = AdminData.objects.get_admin_token()
hep_users = User.objects.filter(hep_id__isnull=False)
for hep_user in hep_users:
product = hep_client.myskillbox_product_for_customer(admin_token, hep_user.hep_id)
if product and License.objects.filter(licensee=hep_user, order_id=product['order_id']).count() == 0:
license = License.objects.create_license_for_role(hep_user, product['activated'], product['raw'],
product['edition'], product['order_id'])
if license.is_valid():
hep_user.license_expiry_date = license.expire_date
hep_user.save()

View File

@ -1,9 +1,15 @@
from datetime import timedelta
from django.apps import apps
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models from django.db import models
from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.auth.models import UserManager as DjangoUserManager
from core.hep_client import TEACHER_EDITION_DURATION, STUDENT_EDITION_DURATION
class RoleManager(models.Manager): class RoleManager(models.Manager):
use_in_migrations = True use_in_migrations = True
@ -66,14 +72,18 @@ class RoleManager(models.Manager):
class UserRoleManager(models.Manager): class UserRoleManager(models.Manager):
def create_role_for_user(self, user, role_key): def get_or_create_role_for_user(self, user, role_key):
from users.models import Role from users.models import Role
try: try:
role = Role.objects.get(key=role_key) role = Role.objects.get(key=role_key)
except Role.DoesNotExist: except Role.DoesNotExist:
return None return None
return self._create_user_role(user, role) try:
user_role = self.model.objects.get(role=role, user=user)
return user_role
except self.model.DoesNotExist:
return self._create_user_role(user, role)
def _create_user_role(self, user, role): def _create_user_role(self, user, role):
user_role = self.model(user=user, role=role) user_role = self.model(user=user, role=role)
@ -82,10 +92,66 @@ class UserRoleManager(models.Manager):
class UserManager(DjangoUserManager): class UserManager(DjangoUserManager):
def create_user_with_random_password(self, first_name, last_name, email):
def _create_user_with_random_password_no_save(self, first_name, last_name, email):
user, created = self.model.objects.get_or_create(email=email, username=email) user, created = self.model.objects.get_or_create(email=email, username=email)
user.first_name = first_name user.first_name = first_name
user.last_name = last_name user.last_name = last_name
user.set_password(self.model.objects.make_random_password()) # Todo: remove if not used
# user.set_password(self.model.objects.make_random_password())
user.set_unusable_password()
return user
def create_user_with_random_password(self, first_name, last_name, email):
user = self._create_user_with_random_password_no_save(first_name, last_name, email)
user.save() user.save()
return user return user
def create_user_from_hep(self, user_data):
try:
user = self.model.objects.get(email=user_data['email'])
user.set_unusable_password()
except self.model.DoesNotExist:
user = self._create_user_with_random_password_no_save( user_data['firstname'],
user_data['lastname'],
user_data['email'])
user.hep_id = user_data['id']
user.hep_group_id = user_data['group_id']
user.save()
return user
class LicenseManager(models.Manager):
def create_license_for_role(self, licensee, activation_date, raw, role, order_id):
Role = apps.get_model('users', 'Role')
if role == 'teacher':
user_role = Role.objects.get_default_teacher_role()
expiry_date = activation_date + timedelta(TEACHER_EDITION_DURATION)
else:
user_role = Role.objects.get_default_student_role()
expiry_date = activation_date + timedelta(STUDENT_EDITION_DURATION)
new_license = self._create_license_for_role(licensee, expiry_date, raw, user_role, order_id)
new_license.licensee.license_expiry_date = new_license.expire_date
new_license.licensee.save()
return new_license
def _create_license_for_role(self, licensee, expiry_date, raw, role, order_id):
return self.create(licensee=licensee, expire_date=expiry_date, raw=raw, for_role=role, order_id=order_id)
def get_active_license_for_user(self, user):
licenses = self.filter(licensee=user, expire_date__gte=timezone.now()).order_by('-expire_date')
if len(licenses) == 0:
return None
license = licenses[0]
# update license on user
user.license_expiry_date = license.expire_date
user.save()
return license

View File

@ -0,0 +1,45 @@
# Generated by Django 2.1.15 on 2020-04-30 12:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('users', '0016_auto_20200304_1250'),
]
operations = [
migrations.CreateModel(
name='License',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('expire_date', models.DateField(null=True)),
('order_id', models.IntegerField(default=-1)),
('raw', models.TextField(default='')),
('for_role', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='users.Role')),
],
),
migrations.AddField(
model_name='user',
name='hep_group_id',
field=models.PositiveIntegerField(null=True),
),
migrations.AddField(
model_name='user',
name='hep_id',
field=models.PositiveIntegerField(null=True),
),
migrations.AddField(
model_name='user',
name='license_expiry_date',
field=models.DateField(default=None, null=True),
),
migrations.AddField(
model_name='license',
name='licensee',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,4 +1,5 @@
import re import re
from datetime import datetime
import string import string
import random import random
@ -8,7 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from users.managers import RoleManager, UserRoleManager, UserManager from core.hep_client import HepClient
from users.managers import RoleManager, UserRoleManager, UserManager, LicenseManager
DEFAULT_SCHOOL_ID = 1 DEFAULT_SCHOOL_ID = 1
@ -17,6 +19,9 @@ class User(AbstractUser):
last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True) last_module = models.ForeignKey('books.Module', related_name='+', on_delete=models.SET_NULL, null=True)
avatar_url = models.CharField(max_length=254, blank=True, default='') avatar_url = models.CharField(max_length=254, blank=True, default='')
email = models.EmailField(_('email address'), unique=True) email = models.EmailField(_('email address'), unique=True)
hep_id = models.PositiveIntegerField(null=True, blank=False)
hep_group_id = models.PositiveIntegerField(null=True, blank=False)
license_expiry_date = models.DateField(blank=False, null=True, default=None)
objects = UserManager() objects = UserManager()
@ -66,11 +71,32 @@ class User(AbstractUser):
else: else:
return None return None
def sync_with_hep_data(self, hep_data):
data_has_changed = False
if self.email != hep_data['email']:
self.email = hep_data['email']
self.username = hep_data['email']
data_has_changed = True
if self.first_name != hep_data['firstname']:
self.first_name = hep_data['firstname']
data_has_changed = True
if self.last_name != hep_data['lastname']:
self.last_name = hep_data['lastname']
data_has_changed = True
if data_has_changed:
self.save()
def set_selected_class(self, school_class): def set_selected_class(self, school_class):
user_settings, created = UserSetting.objects.get_or_create(user=self) user_settings, created = UserSetting.objects.get_or_create(user=self)
user_settings.selected_class = school_class user_settings.selected_class = school_class
user_settings.save() user_settings.save()
@property @property
def full_name(self): def full_name(self):
return self.get_full_name() return self.get_full_name()
@ -111,6 +137,15 @@ class SchoolClass(models.Model):
return '{} {}'.format(prefix, index + 1) return '{} {}'.format(prefix, index + 1)
@classmethod
def create_default_group_for_teacher(cls, user):
default_class_name = cls.generate_default_group_name()
default_class = cls.objects.create(name=default_class_name)
SchoolClassMember.objects.create(
user=user,
school_class=default_class
)
def is_user_in_schoolclass(self, user): def is_user_in_schoolclass(self, user):
return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0 return user.is_superuser or user.school_classes.filter(pk=self.id).count() > 0
@ -201,7 +236,28 @@ class UserSetting(models.Model):
selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE) selected_class = models.ForeignKey(SchoolClass, blank=True, null=True, on_delete=models.CASCADE)
class License(models.Model):
for_role = models.ForeignKey(Role, blank=False, null=True, on_delete=models.CASCADE)
licensee = models.ForeignKey(User, blank=False, null=True, on_delete=models.CASCADE)
expire_date = models.DateField(blank=False, null=True,)
order_id = models.IntegerField(blank=False, null=False, default=-1)
raw = models.TextField(default='')
objects = LicenseManager()
def is_teacher_license(self):
return self.for_role.key == RoleManager.TEACHER_KEY
def is_valid(self):
return HepClient.is_product_active(datetime(self.expire_date.year, self.expire_date.month, self.expire_date.day),
self.for_role.key)
def __str__(self):
return f'License for role: {self.for_role}'
class SchoolClassMember(models.Model): class SchoolClassMember(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE) school_class = models.ForeignKey(SchoolClass, on_delete=models.CASCADE)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)

View File

@ -9,50 +9,78 @@
# @author: chrigu <christian.cueni@iterativ.ch> # @author: chrigu <christian.cueni@iterativ.ch>
import graphene import graphene
from django.conf import settings
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from graphene import relay from graphene import relay
from registration.models import License from core.hep_client import HepClient, HepClientUnauthorizedException, HepClientException
from users.user_signup_login_handler import handle_user_and_verify_products, UNKNOWN_ERROR, EMAIL_NOT_VERIFIED
class LoginError(graphene.ObjectType): class BetaLogin(relay.ClientIDMutation):
field = graphene.String()
class Login(relay.ClientIDMutation):
class Input: class Input:
username_input = graphene.String() username_input = graphene.String()
password_input = graphene.String() password_input = graphene.String()
success = graphene.Boolean() success = graphene.Boolean()
errors = graphene.List(LoginError) # todo: change for consistency message = graphene.String()
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
if settings.ALLOW_BETA_LOGIN:
password = kwargs.get('password_input')
username = kwargs.get('username_input')
user = authenticate(username=username, password=password)
if user is None:
raise Exception('invalid_credentials')
login(info.context, user)
return cls(success=True, message='')
raise Exception('not_implemented')
class Login(relay.ClientIDMutation):
class Input:
token_input = graphene.String()
success = graphene.Boolean()
message = graphene.String()
@classmethod @classmethod
def mutate_and_get_payload(cls, root, info, **kwargs): def mutate_and_get_payload(cls, root, info, **kwargs):
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input')) hep_client = HepClient()
if user is None: token = kwargs.get('token_input')
error = LoginError(field='invalid_credentials')
return cls(success=False, errors=[error])
user_license = None
try: try:
user_license = License.objects.get(licensee=user) user_data = hep_client.customer_me(token)
except License.DoesNotExist: except HepClientUnauthorizedException:
# current users have no license, allow them to login return cls.return_login_message('invalid_credentials')
pass except HepClientException:
return cls.return_login_message(UNKNOWN_ERROR)
if user_license is not None and not user_license.license_type.active: user, status_msg = handle_user_and_verify_products(user_data)
error = LoginError(field='license_inactive') user.sync_with_hep_data(user_data)
return cls(success=False, errors=[error])
login(info.context, user) if user and status_msg != EMAIL_NOT_VERIFIED:
return cls(success=True, errors=[]) login(info.context, user)
if status_msg:
return cls.return_login_message(status_msg)
return cls(success=True, message='success')
@classmethod
def return_login_message(cls, message):
if message == EMAIL_NOT_VERIFIED or message == UNKNOWN_ERROR or message == 'invalid_credentials':
raise Exception(message)
return cls(success=True, message=message)
class UserMutations: class UserMutations:
login = Login.Field() login = Login.Field()
beta_login = BetaLogin.Field()

Some files were not shown because too many files have changed in this diff Show More