diff --git a/config/paths.js b/config/paths.js index bb23b3687..f1be78069 100644 --- a/config/paths.js +++ b/config/paths.js @@ -56,7 +56,6 @@ module.exports = { appPublic: resolveApp('public'), appEntryPoints: { app: resolveModule(resolveApp, 'src/initApp'), - login: resolveModule(resolveApp, 'src/initLogin'), error: resolveModule(resolveApp, 'src/initError') }, appPackageJson: resolveApp('package.json'), diff --git a/config/webpack.config.js b/config/webpack.config.js index 05b810ba0..270b067bc 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -569,16 +569,6 @@ module.exports = function (webpackEnv) { chunks: ['app'], filename: 'beratung-hilfe.html', }), - new HtmlWebpackPlugin({ - title: 'Login', - templateParameters: { - type: 'login', - }, - inject: true, - template: 'src/pages/app.html', - chunks: ['login'], - filename: 'login.html', - }), new HtmlWebpackPlugin({ title: 'Error Page 401', templateParameters: { diff --git a/config/webpackDevServer.config.js b/config/webpackDevServer.config.js index 0b2180a7a..5817fc33b 100644 --- a/config/webpackDevServer.config.js +++ b/config/webpackDevServer.config.js @@ -99,7 +99,7 @@ module.exports = function (proxy, allowedHost) { disableDotRule: true, index: paths.publicUrlOrPath, rewrites: [ - { from: /^\/$/, to: '/login.html' }, + { from: /^\/$/, to: '/beratung-hilfe.html' }, { from: /^\/.+/, to: '/beratung-hilfe.html' }, ] }, diff --git a/cypress/integration/profile.spec.ts b/cypress/integration/profile.spec.ts index 65b1f5871..2e2f22d60 100644 --- a/cypress/integration/profile.spec.ts +++ b/cypress/integration/profile.spec.ts @@ -16,7 +16,7 @@ describe('profile', () => { ); }); - it('can register for a new consulting type with an external agency', () => { + it.skip('can register for a new consulting type with an external agency', () => { cy.intercept( config.endpoints.agencyServiceBase + '?postcode=00000&consultingType=0', @@ -40,7 +40,7 @@ describe('profile', () => { cy.caritasMockedLogin(); cy.visit('/beratung-hilfe.html', { onBeforeLoad(window) { - cy.stub(window, 'open'); + cy.spy(window, 'open').as('windowOpen'); } }); cy.contains('Profil').click(); @@ -59,13 +59,11 @@ describe('profile', () => { ); cy.contains('Jetzt wechseln').click(); - cy.window() - .its('open') - .should( - 'be.calledWith', - 'https://www.onlineberatung-diakonie-baden.de/', - '_blank' - ); + cy.get('@windowOpen').should( + 'be.calledWith', + 'https://www.onlineberatung-diakonie-baden.de/', + '_blank' + ); }); it('can register for a new consulting type with an internal agency', () => { diff --git a/cypress/integration/sessions.spec.ts b/cypress/integration/sessions.spec.ts index 6578214bc..698ad8166 100644 --- a/cypress/integration/sessions.spec.ts +++ b/cypress/integration/sessions.spec.ts @@ -128,7 +128,7 @@ describe('Sessions', () => { cy.get('.sessionsListItem').should('exist'); cy.get('.sessionsList__scrollContainer').scrollTo('bottom'); - cy.get('#loginRoot').should('exist'); + cy.get('.loginForm').should('exist'); }); }); }); diff --git a/cypress/integration/tokens.spec.ts b/cypress/integration/tokens.spec.ts index 4c84e7cfc..c62d0dff0 100644 --- a/cypress/integration/tokens.spec.ts +++ b/cypress/integration/tokens.spec.ts @@ -99,7 +99,7 @@ describe('Keycloak Tokens', () => { waitForTokenProcessing(); cy.tick(1000); // logout() call uses setTimeout - cy.get('#loginRoot').should('exist'); + cy.get('.loginForm').should('exist'); }); //TODO: inspect this test, as there seems to be a race condition @@ -112,7 +112,7 @@ describe('Keycloak Tokens', () => { waitForTokenProcessing(); cy.tick(1000); // logout() call uses setTimeout - cy.get('#loginRoot').should('exist'); + cy.get('.loginForm').should('exist'); }); it('should not logout if refresh token is expired but access token is still valid', () => { @@ -126,7 +126,7 @@ describe('Keycloak Tokens', () => { waitForTokenProcessing(); cy.tick(1000); // logout() call uses setTimeout - cy.get('#loginRoot').should('not.exist'); + cy.get('.loginForm').should('not.exist'); }); it('should not logout if refresh token is expired but access token is still valid when the app loads', () => { @@ -146,6 +146,6 @@ describe('Keycloak Tokens', () => { waitForTokenProcessing(); cy.tick(1000); // logout() call uses setTimeout - cy.get('#loginRoot').should('not.exist'); + cy.get('.loginForm').should('not.exist'); }); }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 2804dbd2d..39fdc635f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -181,9 +181,9 @@ Cypress.Commands.add( cy.intercept('POST', config.endpoints.rejectVideoCall, {}); - cy.visit('login.html'); + cy.visit('/login'); - cy.get('#loginRoot'); + cy.get('.loginForm'); cy.get('#username').type('username', { force: true }); cy.get('#passwordInput').type('password', { force: true diff --git a/index.ts b/index.ts index 07455c8f6..31b02521d 100644 --- a/index.ts +++ b/index.ts @@ -6,7 +6,6 @@ // Page components export { App } from './src/components/app/app'; -export { Login } from './src/components/login/Login'; export { Error } from './src/components/error/Error'; // Component library @@ -20,6 +19,7 @@ export * from './src/components/overlay/Overlay'; export * from './src/components/serviceExplanation/ServiceExplanation'; export * from './src/components/inputField/InputField'; export * from './src/components/agencySelection/agencySelectionHelpers'; +export * from './src/components/profile/AskerRegistrationExternalAgencyOverlay'; // Images export * from './src/resources/img/icons'; diff --git a/nginx.conf b/nginx.conf index a149d0841..c84203ebe 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,7 +12,7 @@ server { server_tokens off; location = / { - index /login.html; + index /beratung-hilfe.html; } location / { diff --git a/package-lock.json b/package-lock.json index 09a1a6468..245ff515d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,23 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apideck/better-ajv-errors": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.2.5.tgz", + "integrity": "sha512-Pm1fAqCT8OEfBVLddU3fWZ/URWpGGhkvlsBIgn9Y2jJlcNumo0gNzPsQswDJTiA8HcKpCjOhWQOgkA9kXR4Ghg==", + "requires": { + "json-schema": "^0.3.0", + "jsonpointer": "^4.1.0", + "leven": "^3.1.0" + }, + "dependencies": { + "json-schema": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.3.0.tgz", + "integrity": "sha512-TYfxx36xfl52Rf1LU9HyWSLGPdYLL+SQ8/E/0yVyKG8wCCDaSrhPap0vEdlsZWRaS6tnKKLPGiEJGiREVC8kxQ==" + } + } + }, "@babel/code-frame": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", @@ -2763,46 +2780,6 @@ } } }, - "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" - }, - "@hapi/formula": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-1.2.0.tgz", - "integrity": "sha512-UFbtbGPjstz0eWHb+ga/GM3Z9EzqKXFWIbSOFURU0A/Gku0Bky4bCk9/h//K2Xr3IrCfjFNhMm4jyZ5dbCewGA==" - }, - "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" - }, - "@hapi/joi": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-16.1.8.tgz", - "integrity": "sha512-wAsVvTPe+FwSrsAurNt5vkg3zo+TblvC5Bb1zMVK6SJzZqw9UrJnexxR+76cpePmtUZKHAPxcQ2Bf7oVHyahhg==", - "requires": { - "@hapi/address": "^2.1.2", - "@hapi/formula": "^1.2.0", - "@hapi/hoek": "^8.2.4", - "@hapi/pinpoint": "^1.0.2", - "@hapi/topo": "^3.1.3" - } - }, - "@hapi/pinpoint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-1.0.2.tgz", - "integrity": "sha512-dtXC/WkZBfC5vxscazuiJ6iq4j9oNx1SHknmIr8hofarpKUZKmlUVYVIhNVzIEgK5Wrc4GMHL5lZtt1uS2flmQ==" - }, - "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", - "requires": { - "@hapi/hoek": "^8.3.0" - } - }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -3248,6 +3225,11 @@ "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz", "integrity": "sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ==" }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, "@types/uglify-js": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.0.tgz", @@ -7234,10 +7216,9 @@ "dev": true }, "dayjs": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz", - "integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g==", - "dev": true + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz", + "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==" }, "debug": { "version": "4.3.1", @@ -10979,6 +10960,11 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" }, + "idb": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.2.tgz", + "integrity": "sha512-1DNDVu3yDhAZkFDlJf0t7r+GLZ248F5pTAtA7V0oVG3yjmV125qZOx3g0XpAEkGZVYQiFDAsSOnGet2bhugc3w==" + }, "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -11771,6 +11757,11 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonpointer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", + "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -11839,6 +11830,11 @@ "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", "dev": true }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -16570,6 +16566,11 @@ } } }, + "react-csv": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.0.3.tgz", + "integrity": "sha512-exyAdFLAxtuM4wNwLYrlKyPYLiJ7e0mv9tqPAd3kq+k1CiJFtznevR3yP0icv5q/y200w+lzNgi7TQn1Wrhu0w==" + }, "react-custom-scrollbars": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz", @@ -16797,6 +16798,14 @@ "react-transition-group": "^4.3.0" } }, + "react-switch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-6.0.0.tgz", + "integrity": "sha512-QV3/6eRK5/5epdQzIqvDAHRoGLbCv/wDpHUi6yBMXY1Xco5XGuIZxvB49PHoV1v/SpEgOCJLD/Zo43iic+aEIw==", + "requires": { + "prop-types": "^15.7.2" + } + }, "react-transition-group": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", @@ -17387,9 +17396,9 @@ } }, "rollup": { - "version": "2.52.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.2.tgz", - "integrity": "sha512-4RlFC3k2BIHlUsJ9mGd8OO+9Lm2eDF5P7+6DNQOp5sx+7N/1tFM01kELfbxlMX3MxT6owvLB1ln4S3QvvQlbUA==", + "version": "2.56.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.2.tgz", + "integrity": "sha512-s8H00ZsRi29M2/lGdm1u8DJpJ9ML8SUOpVVBd33XNeEeL3NVaTiUcSBHzBdF3eAyR0l7VSpsuoVUGrRHq7aPwQ==", "requires": { "fsevents": "~2.3.2" }, @@ -17432,9 +17441,9 @@ "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" }, "terser": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", - "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz", + "integrity": "sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg==", "requires": { "commander": "^2.20.0", "source-map": "~0.7.2", @@ -19085,9 +19094,9 @@ }, "dependencies": { "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "type-fest": { "version": "0.16.0", @@ -21138,34 +21147,36 @@ "dev": true }, "workbox-background-sync": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.1.5.tgz", - "integrity": "sha512-VbUmPLsdz+sLzuNxHvMylzyRTiM4q+q7rwLBk3p2mtRL5NZozI8j/KgoGbno96vs84jx4b9zCZMEOIKEUTPf6w==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.2.4.tgz", + "integrity": "sha512-uoGgm1PZU6THRzXKlMEntrdA4Xkp6SCfxI7re4heN+yGrtAZq6zMKYhZmsdeW+YGnXS3y5xj7WV03b5TDgLh6A==", "requires": { - "workbox-core": "^6.1.5" + "idb": "^6.0.0", + "workbox-core": "6.2.4" } }, "workbox-broadcast-update": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.1.5.tgz", - "integrity": "sha512-zGrTTs+n4wHpYtqYMqBg6kl/x5j1UrczGCQnODSHTxIDV8GXLb/GtA1BCZdysNxpMmdVSeLmTcgIYAAqWFamrA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.2.4.tgz", + "integrity": "sha512-0EpML2lbxNkiZUoap4BJDA0Hfz36MhtUd/rRhFvF6YWoRbTQ8tc6tMaRgM1EBIUmIN2OX9qQlkqe5SGGt4lfXQ==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-build": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.1.5.tgz", - "integrity": "sha512-P+fakR5QFVqJN9l9xHVXtmafga72gh9I+jM3A9HiB/6UNRmOAejXnDgD+RMegOHgQHPwnB44TalMToFaXKWIyA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.2.4.tgz", + "integrity": "sha512-01ZbY1BHi+yYvu4yDGZBw9xm1bWyZW0QGWPxiksvSPAsNH/z/NwgtWW14YEroFyG98mmXb7pufWlwl40zE1KTw==", "requires": { + "@apideck/better-ajv-errors": "^0.2.4", "@babel/core": "^7.11.1", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", - "@hapi/joi": "^16.1.8", "@rollup/plugin-babel": "^5.2.0", "@rollup/plugin-node-resolve": "^11.2.1", "@rollup/plugin-replace": "^2.4.1", "@surma/rollup-plugin-off-main-thread": "^1.4.1", + "ajv": "^8.6.0", "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", @@ -21180,23 +21191,34 @@ "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "^6.1.5", - "workbox-broadcast-update": "^6.1.5", - "workbox-cacheable-response": "^6.1.5", - "workbox-core": "^6.1.5", - "workbox-expiration": "^6.1.5", - "workbox-google-analytics": "^6.1.5", - "workbox-navigation-preload": "^6.1.5", - "workbox-precaching": "^6.1.5", - "workbox-range-requests": "^6.1.5", - "workbox-recipes": "^6.1.5", - "workbox-routing": "^6.1.5", - "workbox-strategies": "^6.1.5", - "workbox-streams": "^6.1.5", - "workbox-sw": "^6.1.5", - "workbox-window": "^6.1.5" - }, - "dependencies": { + "workbox-background-sync": "6.2.4", + "workbox-broadcast-update": "6.2.4", + "workbox-cacheable-response": "6.2.4", + "workbox-core": "6.2.4", + "workbox-expiration": "6.2.4", + "workbox-google-analytics": "6.2.4", + "workbox-navigation-preload": "6.2.4", + "workbox-precaching": "6.2.4", + "workbox-range-requests": "6.2.4", + "workbox-recipes": "6.2.4", + "workbox-routing": "6.2.4", + "workbox-strategies": "6.2.4", + "workbox-streams": "6.2.4", + "workbox-sw": "6.2.4", + "workbox-window": "6.2.4" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -21208,6 +21230,11 @@ "universalify": "^2.0.0" } }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -21219,125 +21246,127 @@ } }, "workbox-cacheable-response": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.1.5.tgz", - "integrity": "sha512-x8DC71lO/JCgiaJ194l9le8wc8lFPLgUpDkLhp2si7mXV6S/wZO+8Osvw1LLgYa8YYTWGbhbFhFTXIkEMknIIA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.2.4.tgz", + "integrity": "sha512-KZSzAOmgWsrk15Wu+geCUSGLIyyzHaORKjH5JnR6qcVZAsm0JXUu2m2OZGqjQ+/eyQwrGdXXqAMW+4wQvTXccg==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.1.5.tgz", - "integrity": "sha512-9SOEle7YcJzg3njC0xMSmrPIiFjfsFm9WjwGd5enXmI8Lwk8wLdy63B0nzu5LXoibEmS9k+aWF8EzaKtOWjNSA==" + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.2.4.tgz", + "integrity": "sha512-Nu8X4R4Is3g8uzEJ6qwbW2CGVpzntW/cSf8OfsQGIKQR0nt84FAKzP2cLDaNLp3L/iV9TuhZgCTZzkMiap5/OQ==" }, "workbox-expiration": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.1.5.tgz", - "integrity": "sha512-6cN+FVbh8fNq56LFKPMchGNKCJeyboHsDuGBqmhDUPvD4uDjsegQpDQzn52VaE0cpywbSIsDF/BSq9E9Yjh5oQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.2.4.tgz", + "integrity": "sha512-EdOBLunrE3+Ff50y7AYDbiwtiLDvB+oEIkL1Wd9G5d176YVqFfgPfMRzJQ7fN+Yy2NfmsFME0Bw+dQruYekWsQ==", "requires": { - "workbox-core": "^6.1.5" + "idb": "^6.0.0", + "workbox-core": "6.2.4" } }, "workbox-google-analytics": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.1.5.tgz", - "integrity": "sha512-LYsJ/VxTkYVLxM1uJKXZLz4cJdemidY7kPyAYtKVZ6EiDG89noASqis75/5lhqM1m3HwQfp2DtoPrelKSpSDBA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.2.4.tgz", + "integrity": "sha512-+PWmTouoGGcDupaxM193F2NmgrF597Pyt9eHIDxfed+x+JSSeUkETlbAKwB8rnBHkAjs8JQcvStEP/IpueNKpQ==", "requires": { - "workbox-background-sync": "^6.1.5", - "workbox-core": "^6.1.5", - "workbox-routing": "^6.1.5", - "workbox-strategies": "^6.1.5" + "workbox-background-sync": "6.2.4", + "workbox-core": "6.2.4", + "workbox-routing": "6.2.4", + "workbox-strategies": "6.2.4" } }, "workbox-navigation-preload": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.1.5.tgz", - "integrity": "sha512-hDbNcWlffv0uvS21jCAC/mYk7NzaGRSWOQXv1p7bj2aONAX5l699D2ZK4D27G8TO0BaLHUmW/1A5CZcsvweQdg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.2.4.tgz", + "integrity": "sha512-y2dOSsaSdEimqhCmBIFR6kBp+GZbtNtWCBaMFwfKxTAul2uyllKcTKBHnZ9IzxULue6o6voV+I2U8Y8tO8n+eA==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-precaching": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.1.5.tgz", - "integrity": "sha512-yhm1kb6wgi141JeM5X7z42XJxCry53tbMLB3NgrxktrZbwbrJF8JILzYy+RFKC9tHC6u2bPmL789GPLT2NCDzw==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.2.4.tgz", + "integrity": "sha512-7POznbVc8EG/mkbXzeb94x3B1VJruPgXvXFgS0NJ3GRugkO4ULs/DpIIb+ycs7uJIKY9EzLS7VXvElr3rMSozQ==", "requires": { - "workbox-core": "^6.1.5", - "workbox-routing": "^6.1.5", - "workbox-strategies": "^6.1.5" + "workbox-core": "6.2.4", + "workbox-routing": "6.2.4", + "workbox-strategies": "6.2.4" } }, "workbox-range-requests": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.1.5.tgz", - "integrity": "sha512-iACChSapzB0yuIum3ascP/+cfBNuZi5DRrE+u4u5mCHigPlwfSWtlaY+y8p+a8EwcDTVTZVtnrGrRnF31SiLqQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.2.4.tgz", + "integrity": "sha512-q4jjTXD1QOKbrHnzV3nxdZtIpOiVoIP5QyVmjuJrybVnAZurtyKcqirTQcAcT/zlTvgwm07zcTTk9o/zIB6DmA==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-recipes": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.1.5.tgz", - "integrity": "sha512-MD1yabHca6O/oj1hrRdfj9cRwhKA5zqIE53rWOAg/dKMMzWQsf9nyRbXRgzK3a13iQvYKuQzURU4Cx58tdnR+Q==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.2.4.tgz", + "integrity": "sha512-z7oECGrt940dw1Bv0xIDJEXY1xARiaxsIedeJOutZFkbgaC/yWG61VTr/hmkeJ8Nx6jnY6W7Rc0iOUvg4sePag==", "requires": { - "workbox-cacheable-response": "^6.1.5", - "workbox-core": "^6.1.5", - "workbox-expiration": "^6.1.5", - "workbox-precaching": "^6.1.5", - "workbox-routing": "^6.1.5", - "workbox-strategies": "^6.1.5" + "workbox-cacheable-response": "6.2.4", + "workbox-core": "6.2.4", + "workbox-expiration": "6.2.4", + "workbox-precaching": "6.2.4", + "workbox-routing": "6.2.4", + "workbox-strategies": "6.2.4" } }, "workbox-routing": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.1.5.tgz", - "integrity": "sha512-uC/Ctz+4GXGL42h1WxUNKxqKRik/38uS0NZ6VY/EHqL2F1ObLFqMHUZ4ZYvyQsKdyI82cxusvhJZHOrY0a2fIQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.2.4.tgz", + "integrity": "sha512-jHnOmpeH4MOWR4eXv6l608npD2y6IFv7yFJ1bT9/RbB8wq2vXHXJQ0ExTZRTWGbVltSG22wEU+MQ8VebDDwDeg==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-strategies": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.1.5.tgz", - "integrity": "sha512-QhiOn9KT9YGBdbfWOmJT6pXZOIAxaVrs6J6AMYzRpkUegBTEcv36+ZhE/cfHoT0u2fxVtthHnskOQ/snEzaXQw==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.2.4.tgz", + "integrity": "sha512-DKgGC3ruceDuu2o+Ae5qmJy0p0q21mFP+RrkdqKrjyf2u8cJvvtvt1eIt4nevKc5BESiKxmhC2h+TZpOSzUDvA==", "requires": { - "workbox-core": "^6.1.5" + "workbox-core": "6.2.4" } }, "workbox-streams": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.1.5.tgz", - "integrity": "sha512-OI1kLvRHGFXV+soDvs6aEwfBwdAkvPB0mRryqdh3/K17qUj/1gRXc8QtpgU+83xqx/I/ar2bTCIj0KPzI/ChCQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.2.4.tgz", + "integrity": "sha512-yG6zV7S2NmYT6koyb7/DoPsyUAat9kD+rOmjP2SbBCtJdLu6ZIi1lgN4/rOkxEby/+Xb4OE4RmCSIZdMyjEmhQ==", "requires": { - "workbox-core": "^6.1.5", - "workbox-routing": "^6.1.5" + "workbox-core": "6.2.4", + "workbox-routing": "6.2.4" } }, "workbox-sw": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.1.5.tgz", - "integrity": "sha512-IMDiqxYbKzPorZLGMUMacLB6r76iVQbdTzYthIZoPfy+uFURJFUtqiWQJKg1L+RMyuYXwKXTahCIGkgFs4jBeg==" + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.2.4.tgz", + "integrity": "sha512-OlWLHNNM+j44sN2OaVXnVcf2wwhJUzcHlXrTrbWDu1JWnrQJ/rLicdc/sbxkZoyE0EbQm7Xr1BXcOjsB7PNlXQ==" }, "workbox-webpack-plugin": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.1.5.tgz", - "integrity": "sha512-tsgeNAYiFP4STNPDxBVT58eiU8nGUmcv7Lq9FFJkQf5MMu6tPw1OLp+KpszhbCWP+R/nEdu85Gjexs6fY647Kg==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.2.4.tgz", + "integrity": "sha512-G6yeOZDYEbtqgNasqwxHFnma0Vp237kMxpsf8JV/YIhvhUuMwnh1WKv4VnFeqmYaWW/ITx0qj92IEMWB/O1mAA==", "requires": { "fast-json-stable-stringify": "^2.1.0", "pretty-bytes": "^5.4.1", "source-map-url": "^0.4.0", "upath": "^1.2.0", "webpack-sources": "^1.4.3", - "workbox-build": "^6.1.5" + "workbox-build": "6.2.4" } }, "workbox-window": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.1.5.tgz", - "integrity": "sha512-akL0X6mAegai2yypnq78RgfazeqvKbsllRtEI4dnbhPcRINEY1NmecFmsQk8SD+zWLK1gw5OdwAOX+zHSRVmeA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.2.4.tgz", + "integrity": "sha512-9jD6THkwGEASj1YP56ZBHYJ147733FoGpJlMamYk38k/EBFE75oc6K3Vs2tGOBx5ZGq54+mHSStnlrtFG3IiOg==", "requires": { - "workbox-core": "^6.1.5" + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.2.4" } }, "worker-farm": { diff --git a/package.json b/package.json index d5a7a364d..c9559e1ea 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "copy-webpack-plugin": "6.4.0", "core-js": "^3.15.0", "css-loader": "5.2.6", + "dayjs": "^1.10.7", "dotenv": "10.0.0", "dotenv-expand": "5.1.0", "draft-js": "0.11.7", @@ -70,6 +71,7 @@ "postcss-safe-parser": "5.0.2", "react": "^17.0.2", "react-app-polyfill": "^2.0.0", + "react-csv": "^2.0.3", "react-datepicker": "4.1.1", "react-dev-utils": "^11.0.4", "react-device-detect": "^1.17.0", @@ -77,6 +79,7 @@ "react-refresh": "^0.10.0", "react-router-dom": "5.2.0", "react-select": "4.3.1", + "react-switch": "^6.0.0", "resolve": "1.20.0", "resolve-url-loader": "^4.0.0", "sanitize-html": "^2.6.0", @@ -95,7 +98,7 @@ "webpack-dev-server": "3.11.2", "webpack-manifest-plugin": "3.1.1", "whatwg-fetch": "^3.6.2", - "workbox-webpack-plugin": "6.1.5" + "workbox-webpack-plugin": "^6.2.4" }, "devDependencies": { "@commitlint/cli": "13.0.0", diff --git a/src/api/apiGetConsultantStatistics.ts b/src/api/apiGetConsultantStatistics.ts new file mode 100644 index 000000000..54570aa13 --- /dev/null +++ b/src/api/apiGetConsultantStatistics.ts @@ -0,0 +1,30 @@ +import { config } from '../resources/scripts/config'; +import { fetchData, FETCH_METHODS } from './fetchData'; + +export interface ConsultantStatisticsDTO { + startDate: string; + endDate: string; + numberOfAssignedSessions: number; + numberOfSentMessages: number; + numberOfSessionsWhereConsultantWasActive: number; + videoCallDuration: number; +} + +export interface ApiGetConsultantStatisticsInterface { + startDate: string; + endDate: string; +} + +export const apiGetConsultantStatistics = async ({ + startDate, + endDate +}: ApiGetConsultantStatisticsInterface): Promise => { + const url = + config.endpoints.consultantStatistics + + `?startDate=${startDate}&endDate=${endDate}`; + + return fetchData({ + url: url, + method: FETCH_METHODS.GET + }); +}; diff --git a/src/api/apiTwoFactorAuth.ts b/src/api/apiTwoFactorAuth.ts new file mode 100644 index 000000000..24d679be6 --- /dev/null +++ b/src/api/apiTwoFactorAuth.ts @@ -0,0 +1,25 @@ +import { config } from '../resources/scripts/config'; +import { fetchData, FETCH_ERRORS, FETCH_METHODS } from './fetchData'; + +export const apiPutTwoFactorAuth = async (body: { + secret: string; + initialCode: string; +}): Promise => { + const url = config.endpoints.twoFactorAuth; + + return fetchData({ + url: url, + method: FETCH_METHODS.PUT, + bodyData: JSON.stringify(body), + responseHandling: [FETCH_ERRORS.BAD_REQUEST] + }); +}; + +export const apiDeleteTwoFactorAuth = async (): Promise => { + const url = config.endpoints.twoFactorAuth; + + return fetchData({ + url: url, + method: FETCH_METHODS.DELETE + }); +}; diff --git a/src/api/fetchData.ts b/src/api/fetchData.ts index 64ff2e4ce..7405db7b0 100644 --- a/src/api/fetchData.ts +++ b/src/api/fetchData.ts @@ -5,7 +5,7 @@ import { redirectToErrorPage } from '../components/error/errorHandling'; import { logout } from '../components/logout/logout'; -import { CSRF_WHITELIST_HEADER } from '../resources/scripts/config'; +import { config, CSRF_WHITELIST_HEADER } from '../resources/scripts/config'; const nodeEnv: string = process.env.NODE_ENV as string; const isLocalDevelopment = nodeEnv === 'development'; @@ -19,13 +19,14 @@ export const FETCH_METHODS = { export const FETCH_ERRORS = { ABORT: 'ABORT', - EMPTY: 'EMPTY', BAD_REQUEST: 'BAD_REQUEST', + CATCH_ALL: 'CATCH_ALL', CONFLICT: 'CONFLICT', CONFLICT_WITH_RESPONSE: 'CONFLICT_WITH_RESPONSE', - TIMEOUT: 'TIMEOUT', + EMPTY: 'EMPTY', NO_MATCH: 'NO_MATCH', - CATCH_ALL: 'CATCH_ALL', + TIMEOUT: 'TIMEOUT', + UNAUTHORIZED: 'UNAUTHORIZED', X_REASON: 'X-Reason' }; @@ -147,7 +148,7 @@ export const fetchData = (props: FetchDataProps): Promise => : new Error(FETCH_ERRORS.CONFLICT) ); } else if (response.status === 401) { - logout(true); + logout(true, config.urls.toLogin); } } else { const error = getErrorCaseForStatus(response.status); diff --git a/src/api/index.ts b/src/api/index.ts index 211832a46..023723692 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,11 +8,12 @@ export * from './apiGetAgencyConsultantList'; export * from './apiGetAgencyId'; export * from './apiGetAskerSessionList'; export * from './apiGetConsultantSessionList'; +export * from './apiGetConsultantStatistics'; +export * from './apiGetConsultingType'; +export * from './apiGetConsultingTypes'; export * from './apiGetGroupChatInfo'; export * from './apiGetGroupMembers'; export * from './apiGetMonitoring'; -export * from './apiGetConsultingType'; -export * from './apiGetConsultingTypes'; export * from './apiGetSessionData'; export * from './apiGetUserData'; export * from './apiGroupChatSettings'; @@ -34,6 +35,7 @@ export * from './apiSessionAssign'; export * from './apiSetAbsence'; export * from './apiSetSessionRead'; export * from './apiStartVideoCall'; +export * from './apiTwoFactorAuth'; export * from './apiUpdateMonitoring'; export * from './apiUpdatePassword'; export * from './apiUploadAttachment'; diff --git a/src/components/app/AuthenticatedApp.tsx b/src/components/app/AuthenticatedApp.tsx index 769178dc7..e36f71e40 100644 --- a/src/components/app/AuthenticatedApp.tsx +++ b/src/components/app/AuthenticatedApp.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useContext, useEffect, useState } from 'react'; +import { ComponentType, useContext, useEffect, useState } from 'react'; import { Routing } from './Routing'; import { config } from '../../resources/scripts/config'; import { @@ -25,12 +25,14 @@ import { Loading } from './Loading'; import { handleTokenRefresh } from '../auth/auth'; import { logout } from '../logout/logout'; import { Notifications } from '../notifications/Notifications'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; import './authenticatedApp.styles'; import './navigation.styles'; interface AuthenticatedAppProps { onAppReady: Function; onLogout: Function; + legalComponent: ComponentType; } export const AuthenticatedApp = (props: AuthenticatedAppProps) => { @@ -69,7 +71,7 @@ export const AuthenticatedApp = (props: AuthenticatedAppProps) => { setAppReady(true); }) .catch((error) => { - window.location.href = config.urls.toLogin; + window.location.href = config.urls.toEntry; console.log(error); }); }); @@ -94,7 +96,10 @@ export const AuthenticatedApp = (props: AuthenticatedAppProps) => { if (appReady) { return ( <> - + {notifications && ( )} diff --git a/src/components/app/LoginLoader.tsx b/src/components/app/LoginLoader.tsx index 4409c5a1b..bd381a254 100644 --- a/src/components/app/LoginLoader.tsx +++ b/src/components/app/LoginLoader.tsx @@ -5,6 +5,7 @@ import { apiGetConsultingType } from '../../api'; import { Login } from '../login/Login'; import { StageProps } from '../stage/stage'; import { APP_PATH } from '../../resources/scripts/config'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; // Avoid matching strings like "beratung-hilfe.html" // where we already know it's not a consulting type. @@ -17,17 +18,24 @@ const isValidConsultingTypeSlug = (slug: string): boolean => { export interface LoginLoaderProps { handleUnmatch: () => void; + legalComponent: ComponentType; stageComponent: ComponentType; } export const LoginLoader = ({ handleUnmatch, + legalComponent, stageComponent }: LoginLoaderProps) => { const [isValidResort, setIsValidResort] = useState(); const { consultingTypeSlug } = useParams(); useEffect(() => { + if (consultingTypeSlug === 'login') { + setIsValidResort(true); + return; + } + if (!isValidConsultingTypeSlug(consultingTypeSlug)) { handleUnmatch(); return; @@ -40,7 +48,12 @@ export const LoginLoader = ({ }, [consultingTypeSlug, handleUnmatch]); if (isValidResort) { - return ; + return ( + + ); } else { return null; } diff --git a/src/components/app/Routing.tsx b/src/components/app/Routing.tsx index d69bafb6e..933ed3edb 100644 --- a/src/components/app/Routing.tsx +++ b/src/components/app/Routing.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useContext, useEffect, useMemo } from 'react'; +import { ComponentType, useContext, useEffect, useMemo } from 'react'; import { Route } from 'react-router-dom'; import { RouterConfigUser, @@ -21,9 +21,11 @@ import { SessionsListWrapper } from '../sessionsList/SessionsListWrapper'; import { NavigationBar } from './NavigationBar'; import { Header } from '../header/Header'; import { FinishedAnonymousConversationHandler } from './FinishedAnonymousConversationHandler'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; interface routingProps { logout?: Function; + legalComponent: ComponentType; } export const Routing = (props: routingProps) => { @@ -106,8 +108,9 @@ export const Routing = (props: routingProps) => { exact key={index} path={route.path} - component={(props) => ( + component={(componentProps) => ( @@ -115,7 +118,7 @@ export const Routing = (props: routingProps) => { /> ) ), - [routerConfig.detailRoutes] + [routerConfig.detailRoutes, props] )} {((hasUserProfileRoutes) => { @@ -157,7 +160,7 @@ export const Routing = (props: routingProps) => { exact key={index} path={route.path} - component={(props) => ( + component={() => ( { /> ) ), - [routerConfig.profileRoutes] + [routerConfig.profileRoutes, props] )} diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index f51dd0d23..a2d4ae845 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -1,6 +1,6 @@ import '../../polyfill'; import * as React from 'react'; -import { ComponentType, ReactNode, useState } from 'react'; +import { ComponentType, ReactNode, useEffect, useState } from 'react'; import { Router, Switch, Route } from 'react-router-dom'; import { createBrowserHistory } from 'history'; import { AuthenticatedApp } from './AuthenticatedApp'; @@ -11,15 +11,23 @@ import '../../resources/styles/styles'; import { WaitingRoomLoader } from '../waitingRoom/WaitingRoomLoader'; import { ContextProvider } from '../../globalState/state'; import { WebsocketHandler } from './WebsocketHandler'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; export const history = createBrowserHistory(); interface AppProps { stageComponent: ComponentType; + legalComponent: ComponentType; + entryPoint: string; extraRoutes?: ReactNode; } -export const App = ({ stageComponent, extraRoutes }: AppProps) => { +export const App = ({ + stageComponent, + legalComponent, + entryPoint, + extraRoutes +}: AppProps) => { // The login is possible both at the root URL as well as with an // optional resort name. Since resort names are dynamic, we have // to find out if the provided path is a resort name. If not, we @@ -39,6 +47,20 @@ export const App = ({ stageComponent, extraRoutes }: AppProps) => { const [startWebsocket, setStartWebsocket] = useState(false); const [disconnectWebsocket, setDisconnectWebsocket] = useState(false); + const [isInitiallyLoaded, setIsInitiallyLoaded] = useState(false); + + const activateInitialRedirect = () => { + setIsInitiallyLoaded(true); + history.push(entryPoint); + }; + + useEffect(() => { + if (!isInitiallyLoaded && window.location.pathname === '/') { + activateInitialRedirect(); + } else { + setIsInitiallyLoaded(true); + } + }, []); // eslint-disable-line return ( @@ -57,6 +79,7 @@ export const App = ({ stageComponent, extraRoutes }: AppProps) => { ) } stageComponent={stageComponent} + legalComponent={legalComponent} /> )} @@ -73,19 +96,23 @@ export const App = ({ stageComponent, extraRoutes }: AppProps) => { )} {!hasUnmatchedLoginConsultingType && ( - + setHasUnmatchedLoginConsultingType(true) } stageComponent={stageComponent} + legalComponent={legalComponent} /> )} - setStartWebsocket(true)} - onLogout={() => setDisconnectWebsocket(true)} - /> + {isInitiallyLoaded && ( + setStartWebsocket(true)} + onLogout={() => setDisconnectWebsocket(true)} + legalComponent={legalComponent} + /> + )} diff --git a/src/components/auth/auth.ts b/src/components/auth/auth.ts index ad6b562f3..730e63405 100644 --- a/src/components/auth/auth.ts +++ b/src/components/auth/auth.ts @@ -1,3 +1,4 @@ +import { config } from '../../resources/scripts/config'; import { logout } from '../logout/logout'; import { setValueInCookie } from '../sessionCookie/accessSessionCookie'; import { @@ -38,7 +39,7 @@ const refreshTokens = (): Promise => { tokenExpiry.refreshTokenValidUntilTime <= currentTime - RENEW_BEFORE_EXPIRY_IN_MS ) { - logout(true); + logout(true, config.urls.toLogin); return Promise.resolve(); } @@ -78,7 +79,7 @@ const startTimers = ({ window.clearInterval(refreshInterval); } - logout(true); + logout(true, config.urls.toLogin); }, refreshTokenValidInMs); } }; @@ -95,7 +96,7 @@ export const handleTokenRefresh = (): Promise => { if (refreshTokenValidInMs <= 0 && accessTokenValidInMs <= 0) { // access token and refresh token no longer valid, logout - logout(true); + logout(true, config.urls.toLogin); resolve(); } else if (accessTokenValidInMs <= 0) { // access token no longer valid but refresh token still valid, refresh tokens diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx index 05d9c938a..a60f746c5 100644 --- a/src/components/button/Button.tsx +++ b/src/components/button/Button.tsx @@ -19,7 +19,12 @@ export interface ButtonItem { icon?: JSX.Element; id?: string; label?: string; - smallIconBackgroundColor?: 'green' | 'red' | 'yellow' | 'grey'; + smallIconBackgroundColor?: + | 'green' + | 'red' + | 'yellow' + | 'grey' + | 'alternate'; title?: string; type: string; } diff --git a/src/components/button/button.styles.scss b/src/components/button/button.styles.scss index 4e4a59f22..8155c4702 100644 --- a/src/components/button/button.styles.scss +++ b/src/components/button/button.styles.scss @@ -230,6 +230,23 @@ $buttonHeight: 50px; } } } + + &--alternate { + background-color: $button-small-icon-alternate-background-color; + box-shadow: $button-box-shadow-grey; + border: none; + + &:hover { + background-color: darken( + $button-small-icon-alternate-background-color, + 7% + ); + + svg { + fill: $white; + } + } + } } &__item--disabled { diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx index 1ad5bdca4..fb6f5e98c 100644 --- a/src/components/checkbox/Checkbox.tsx +++ b/src/components/checkbox/Checkbox.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import { ComponentType } from 'react'; import { ReactComponent as CheckmarkIcon } from '../../resources/img/icons/checkmark.svg'; +import { Text } from '../text/Text'; import './checkbox.styles'; export interface CheckboxItem { @@ -8,10 +10,25 @@ export interface CheckboxItem { labelId: string; label: string; checked: boolean; + complexLabel?: { + prefix: string; + suffix: string; + component: ComponentType; + attributes: Object; + }; } export const Checkbox = (props) => { const checkboxItem = props.item; + + const preparedLabel = checkboxItem.complexLabel + ? null + : { + dangerouslySetInnerHTML: { + __html: checkboxItem.label + } + }; + return (
{ id={checkboxItem.labelId} className="checkbox__label" htmlFor={checkboxItem.inputId} - dangerouslySetInnerHTML={{ - __html: checkboxItem.label - }} - > + {...preparedLabel} + > + {checkboxItem.complexLabel && ( + + + + + + )} +
); }; diff --git a/src/components/checkbox/checkbox.styles.scss b/src/components/checkbox/checkbox.styles.scss index 553258866..5d1fb77dd 100644 --- a/src/components/checkbox/checkbox.styles.scss +++ b/src/components/checkbox/checkbox.styles.scss @@ -17,6 +17,17 @@ $checkbox-size: 24px; color: $secondary; margin: 0 0 0 12px; line-height: 24px; + + &--complex { + .text:first-of-type, + div { + float: left; + } + + div { + margin: 0 3px; + } + } } &__input { diff --git a/src/components/editableData/editableData.styles.scss b/src/components/editableData/editableData.styles.scss index 9a03e5d50..415e6c385 100644 --- a/src/components/editableData/editableData.styles.scss +++ b/src/components/editableData/editableData.styles.scss @@ -73,7 +73,7 @@ &__label--invalid { .text { - color: $primary; + color: $form-error; } } diff --git a/src/components/error/Error.tsx b/src/components/error/Error.tsx index 849eab927..4bc79ea55 100644 --- a/src/components/error/Error.tsx +++ b/src/components/error/Error.tsx @@ -6,6 +6,7 @@ import { ReactComponent as Icon404 } from '../../resources/img/illustrations/ooh import { ReactComponent as Icon500 } from '../../resources/img/illustrations/gleich-zurueck.svg'; import { translate } from '../../utils/translate'; import { Button, BUTTON_TYPES } from '../button/Button'; +import { config } from '../../resources/scripts/config'; import '../../resources/styles/styles'; import './error.styles'; @@ -18,7 +19,7 @@ export const Error = () => { const statusCode = getStatusCode(); const buttonHandle = () => { - document.location.href = '/'; + document.location.href = config.urls.toLogin; }; let Icon; diff --git a/src/components/groupChat/JoinGroupChatView.tsx b/src/components/groupChat/JoinGroupChatView.tsx index bf0293182..dc95b3be6 100644 --- a/src/components/groupChat/JoinGroupChatView.tsx +++ b/src/components/groupChat/JoinGroupChatView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useContext, useState } from 'react'; +import { useEffect, useContext, useState, ComponentType } from 'react'; import { UserDataContext, ActiveSessionGroupIdContext, @@ -46,8 +46,13 @@ import { ReactComponent as WarningIcon } from '../../resources/img/icons/i.svg'; import './joinChat.styles'; import { Headline } from '../headline/Headline'; import { Text } from '../text/Text'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; -export const JoinGroupChatView = () => { +interface JoinGroupChatViewProps { + legalComponent: ComponentType; +} + +export const JoinGroupChatView = (props: JoinGroupChatViewProps) => { const { userData } = useContext(UserDataContext); const { sessionsData, setSessionsData } = useContext(SessionsDataContext); const { setStoppedGroupChat } = useContext(StoppedGroupChatContext); @@ -203,7 +208,7 @@ export const JoinGroupChatView = () => { return (
- +
{ return (
- + {!hideImprint && ( + + + + )} + {!hideImprint && showDivider && ( - - - + )} +
diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx index 776ce8d3e..bdfdae56a 100644 --- a/src/components/login/Login.tsx +++ b/src/components/login/Login.tsx @@ -9,10 +9,15 @@ import { autoLogin } from '../registration/autoLogin'; import { Text } from '../text/Text'; import { ReactComponent as PersonIcon } from '../../resources/img/icons/person.svg'; import { ReactComponent as LockIcon } from '../../resources/img/icons/lock.svg'; +import { ReactComponent as VerifiedIcon } from '../../resources/img/icons/verified.svg'; import { StageProps } from '../stage/stage'; import { StageLayout } from '../stageLayout/StageLayout'; +import { FETCH_ERRORS } from '../../api'; +import { OTP_LENGTH } from '../profile/TwoFactorAuth'; +import clsx from 'clsx'; import '../../resources/styles/styles'; import './login.styles'; +import { LegalInformationLinksProps } from './LegalInformationLinks'; const loginButton: ButtonItem = { label: translate('login.button.label'), @@ -20,26 +25,41 @@ const loginButton: ButtonItem = { }; interface LoginProps { + legalComponent: ComponentType; stageComponent: ComponentType; } -export const Login = ({ stageComponent: Stage }: LoginProps) => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [isButtonDisabled, setIsButtonDisabled] = useState( +export const Login = ({ + legalComponent, + stageComponent: Stage +}: LoginProps) => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [isButtonDisabled, setIsButtonDisabled] = useState( username.length > 0 && password.length > 0 ); - const [showLoginError, setShowLoginError] = useState(false); - const [isRequestInProgress, setIsRequestInProgress] = useState(false); + const [otp, setOtp] = useState(''); + const [isOtpRequired, setIsOtpRequired] = useState(false); + const [showLoginError, setShowLoginError] = useState(''); + const [isRequestInProgress, setIsRequestInProgress] = + useState(false); useEffect(() => { - setShowLoginError(false); - if (username && password) { + setShowLoginError(''); + if ( + (!isOtpRequired && username && password) || + (isOtpRequired && username && password && otp) + ) { setIsButtonDisabled(false); } else { setIsButtonDisabled(true); } - }, [username, password]); + }, [username, password, otp, isOtpRequired]); + + useEffect(() => { + setOtp(''); + setIsOtpRequired(false); + }, [username]); const inputItemUsername: InputFieldItem = { name: 'username', @@ -60,6 +80,17 @@ export const Login = ({ stageComponent: Stage }: LoginProps) => { icon: }; + const otpInputItem: InputFieldItem = { + content: otp, + id: 'otp', + infoText: translate('login.warning.failed.otp.missing'), + label: translate('twoFactorAuth.activate.step3.input.label'), + name: 'otp', + type: 'text', + icon: , + maxLength: OTP_LENGTH + }; + const handleUsernameChange = (event) => { setUsername(event.target.value); }; @@ -68,15 +99,54 @@ export const Login = ({ stageComponent: Stage }: LoginProps) => { setPassword(event.target.value); }; - const handleLoginError = () => { - setShowLoginError(true); - setIsRequestInProgress(false); + const handleOtpChange = (event) => { + setOtp(event.target.value); }; const handleLogin = () => { - if (!isRequestInProgress && username && password) { + if (!isRequestInProgress && !isOtpRequired && username && password) { setIsRequestInProgress(true); - autoLogin({ username, password, redirect: true, handleLoginError }); + autoLogin({ + username: username, + password: password, + redirect: true + }) + .catch((error) => { + if (error.message === FETCH_ERRORS.UNAUTHORIZED) { + setShowLoginError( + translate('login.warning.failed.unauthorized') + ); + } else if (error.message === FETCH_ERRORS.BAD_REQUEST) { + setIsOtpRequired(true); + } + }) + .finally(() => { + setIsRequestInProgress(false); + }); + } else if ( + !isRequestInProgress && + isOtpRequired && + username && + password && + otp + ) { + setIsRequestInProgress(true); + autoLogin({ + username, + password, + redirect: true, + otp + }) + .catch((error) => { + if (error.message === FETCH_ERRORS.UNAUTHORIZED) { + setShowLoginError( + translate('login.warning.failed.unauthorized.otp') + ); + } + }) + .finally(() => { + setIsRequestInProgress(false); + }); } }; @@ -87,7 +157,11 @@ export const Login = ({ stageComponent: Stage }: LoginProps) => { }; return ( - } showLegalLinks> + } + showLegalLinks + >

{translate('login.headline')}

@@ -102,13 +176,24 @@ export const Login = ({ stageComponent: Stage }: LoginProps) => { inputHandle={handlePasswordChange} keyUpHandle={handleKeyUp} /> - {showLoginError ? ( +
+ +
+ {showLoginError && ( - ) : null} + )} { - const redirectUrl = altRedirectUrl ? altRedirectUrl : config.urls.toLogin; + const redirectUrl = altRedirectUrl ? altRedirectUrl : config.urls.toEntry; setTimeout(() => { window.location.href = redirectUrl; }, 1000); diff --git a/src/components/message/FurtherSteps.tsx b/src/components/message/FurtherSteps.tsx index cca60cff8..bf44617de 100644 --- a/src/components/message/FurtherSteps.tsx +++ b/src/components/message/FurtherSteps.tsx @@ -122,12 +122,15 @@ export const FurtherSteps = (props: FurtherStepsProps) => { { label: translate('furtherSteps.email.overlay.button2.label'), function: OVERLAY_FUNCTIONS.CLOSE, - type: BUTTON_TYPES.LINK + type: BUTTON_TYPES.SECONDARY } ], headline: translate('furtherSteps.email.overlay.headline'), nestedComponent: ( - + handleEmailChange(e)} + /> ), svg: EnvelopeIllustration }; diff --git a/src/components/message/MessageItemComponent.tsx b/src/components/message/MessageItemComponent.tsx index 5f3c9e89a..1c78cb20e 100644 --- a/src/components/message/MessageItemComponent.tsx +++ b/src/components/message/MessageItemComponent.tsx @@ -30,9 +30,10 @@ import { import { VideoCallMessage } from './VideoCallMessage'; import { FurtherSteps } from './FurtherSteps'; import { MessageAttachment } from './MessageAttachment'; -import './message.styles'; import { isVoluntaryInfoSet } from './messageHelpers'; +import { Text } from '../text/Text'; import { translate } from '../../utils/translate'; +import './message.styles'; enum MessageType { FURTHER_STEPS = 'FURTHER_STEPS', @@ -140,9 +141,14 @@ export const MessageItemComponent = ({ if (messageDate) { return (
- {typeof messageDate === 'number' - ? getPrettyDateFromMessageDate(messageDate) - : messageDate} +
); } diff --git a/src/components/message/VoluntaryInfoOverlay.tsx b/src/components/message/VoluntaryInfoOverlay.tsx index ef46133d3..9870a9ece 100644 --- a/src/components/message/VoluntaryInfoOverlay.tsx +++ b/src/components/message/VoluntaryInfoOverlay.tsx @@ -161,7 +161,7 @@ export const VoluntaryInfoOverlay = (props: VoluntaryInfoOverlayProps) => { 'furtherSteps.voluntaryInfo.overlay.button2.label' ), function: OVERLAY_FUNCTIONS.CLOSE, - type: BUTTON_TYPES.LINK + type: BUTTON_TYPES.SECONDARY } ], headline: translate('furtherSteps.voluntaryInfo.overlay.headline'), diff --git a/src/components/message/message.styles.scss b/src/components/message/message.styles.scss index 4f041ea22..c439a8cfc 100644 --- a/src/components/message/message.styles.scss +++ b/src/components/message/message.styles.scss @@ -106,14 +106,8 @@ $message-lineheight: 21px; display: flex; align-items: center; width: 200px; - font-family: $font-family-divider; - font-size: $font-size-secondary; - line-height: $line-height-secondary; text-align: center; margin: $grid-base-four auto $grid-base-two; - color: $message-item-divider-color; - font-weight: $message-item-divider-font-weight; - letter-spacing: $message-item-divider-letter-spacing; @include breakpoint($fromLarge) { width: 280px; @@ -123,7 +117,7 @@ $message-lineheight: 21px; &::after { content: ''; display: inline-block; - border-top: 1px solid $message-item-divider-line-color; + border-top: 1px solid $text-divider-color; width: $grid-base-three; vertical-align: middle; margin: 0 $grid-base; diff --git a/src/components/overlay/Overlay.tsx b/src/components/overlay/Overlay.tsx index 46a2b3b2f..4a1073a72 100644 --- a/src/components/overlay/Overlay.tsx +++ b/src/components/overlay/Overlay.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import * as ReactDOM from 'react-dom'; import { ButtonItem, Button } from '../button/Button'; import { Text } from '../text/Text'; import { Headline, HeadlineLevel } from '../headline/Headline'; +import { ReactComponent as XIcon } from '../../resources/img/icons/x.svg'; +import { translate } from '../../utils/translate'; import clsx from 'clsx'; import './overlay.styles'; @@ -19,6 +21,8 @@ export const OVERLAY_FUNCTIONS = { STOP_GROUP_CHAT: 'STOP_GROUP_CHAT', LEAVE_GROUP_CHAT: 'LEAVE_GROUP_CHAT', DELETE_ACCOUNT: 'DELETE_ACCOUNT', + NEXT_STEP: 'NEXT_STEP', + PREV_STEP: 'PREV_STEP', DELETE_SESSION: 'DELETE_SESSION', FINISH_ANONYMOUS_CONVERSATION: 'FINISH_ANONYMOUS_CONVERSATION', ARCHIVE: 'ARCHIVE' @@ -36,6 +40,13 @@ export interface OverlayItem { svg?: React.FunctionComponent< React.SVGProps & { title?: string } >; + handleOverlay?: Function; + step?: { + icon: React.FunctionComponent< + React.SVGProps & { title?: string } + >; + label: string; + }; } export const OverlayWrapper = (props) => { @@ -45,9 +56,26 @@ export const OverlayWrapper = (props) => { export const Overlay = (props: { className?: string; - item: OverlayItem; - handleOverlay: Function; + item?: OverlayItem; + handleOverlay?: Function; + handleOverlayClose?: Function; + items?: OverlayItem[]; }) => { + const [activeStep, setActiveStep] = useState(0); + const [activeOverlay, setActiveOverlay] = useState( + props.item + ? { ...props.item, ...props.handleOverlay } + : props.items[activeStep] + ); + + useEffect(() => { + setActiveOverlay( + props.item + ? { ...props.item, ...props.handleOverlay } + : props.items[activeStep] + ); + }, [props.item, props.items]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { document.querySelector('.app')?.classList.add('app--blur'); @@ -56,48 +84,115 @@ export const Overlay = (props: { }; }, []); + useEffect(() => { + if (props.items) { + setActiveOverlay(props.items[activeStep]); + } + }, [activeStep, props.items]); + const handleButtonClick = (buttonFunction: string) => { - props.handleOverlay(buttonFunction); + if (buttonFunction === OVERLAY_FUNCTIONS.NEXT_STEP) { + setActiveStep(activeStep + 1); + } else if (buttonFunction === OVERLAY_FUNCTIONS.PREV_STEP) { + setActiveStep(activeStep - 1); + } else if (props.item && props.handleOverlay) { + props.handleOverlay(buttonFunction); + } else if (activeOverlay.handleOverlay) { + activeOverlay.handleOverlay(buttonFunction); + } }; - const item = props.item; - const Illustration = item.svg; + const getOverlayHeadline = () => { + if (props.items?.some((item) => item.step)) { + return ` + + + ${activeStep + 1}${translate('overlay.step.headline.prefix')} + + ${activeOverlay.headline} + + `; + } else return activeOverlay.headline; + }; + + const Illustration = activeOverlay.svg; return (
- {item.svg && ( + {props.handleOverlayClose && ( + props.handleOverlayClose(e)} + /> + )} + {props.items?.some((item) => item.step) && ( +
+ {props.items.map((item, i) => { + if (item.step) { + const StepIcon = item.step?.icon; + return ( +
activeStep + })} + key={i} + > +
+
+ +
+ +
+
+ ); + } else return null; + })} +
+ )} + {activeOverlay.svg && (
)} - {item.headline && ( + {activeOverlay.headline && ( )} - {item.copy && } - {item.nestedComponent && ( + {activeOverlay.copy && ( + + )} + {activeOverlay.nestedComponent && (
- {item.nestedComponent} + {activeOverlay.nestedComponent}
)} - {item.buttonSet && item.buttonSet.length > 0 && ( + {activeOverlay.buttonSet && activeOverlay.buttonSet.length > 0 && (
- {item.buttonSet?.map((item, i) => ( + {activeOverlay.buttonSet?.map((item, i) => (
{hasUserAuthority( AUTHORITIES.CONSULTANT_DEFAULT, @@ -111,6 +124,7 @@ export const Profile = () => { text={translate('profile.data.title')} type="divider" /> +
@@ -129,22 +143,7 @@ export const Profile = () => { )}
diff --git a/src/components/profile/TwoFactorAuth.tsx b/src/components/profile/TwoFactorAuth.tsx new file mode 100644 index 000000000..c795bb7c6 --- /dev/null +++ b/src/components/profile/TwoFactorAuth.tsx @@ -0,0 +1,372 @@ +import * as React from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { UserDataContext, UserDataInterface } from '../../globalState'; +import { translate } from '../../utils/translate'; +import { Headline } from '../headline/Headline'; +import { Text } from '../text/Text'; +import Switch from 'react-switch'; +import { + Overlay, + OverlayItem, + OverlayWrapper, + OVERLAY_FUNCTIONS +} from '../overlay/Overlay'; +import { BUTTON_TYPES } from '../button/Button'; +import { + InputField, + InputFieldItem, + InputFieldLabelState +} from '../inputField/InputField'; +import { ReactComponent as DownloadIcon } from '../../resources/img/icons/download.svg'; +import { ReactComponent as AddIcon } from '../../resources/img/icons/add.svg'; +import { ReactComponent as UrlIcon } from '../../resources/img/icons/url.svg'; +import { ReactComponent as CheckIcon } from '../../resources/img/icons/checkmark.svg'; +import { + apiDeleteTwoFactorAuth, + apiGetUserData, + apiPutTwoFactorAuth, + FETCH_ERRORS +} from '../../api'; +import './twoFactorAuth.styles'; + +export const OTP_LENGTH = 6; + +export const TwoFactorAuth = () => { + const { userData, setUserData } = useContext(UserDataContext); + const [isSwitchChecked, setIsSwitchChecked] = useState( + userData.twoFactorAuth.isActive + ); + const [overlayActive, setOverlayActive] = useState(false); + const [otp, setOtp] = useState(''); + const defaultOtpLabel = translate( + 'twoFactorAuth.activate.step3.input.label' + ); + const [otpLabel, setOtpLabel] = useState(defaultOtpLabel); + const [otpLabelState, setOtpLabelState] = useState(); + const [otpInputInfo, setOtpInputInfo] = useState(''); + const [isRequestInProgress, setIsRequestInProgress] = + useState(false); + + useEffect(() => { + setOverlayItems(twoFactorAuthStepsOverlay); + }, [otp, otpLabel, otpLabelState]); // eslint-disable-line react-hooks/exhaustive-deps + + const updateUserData = () => { + apiGetUserData() + .then((newUserData: UserDataInterface) => { + setUserData(newUserData); + }) + .catch((error) => console.log(error)); + }; + + const handleSwitchChange = () => { + if (!isSwitchChecked) { + setIsSwitchChecked(true); + setOverlayActive(true); + } else { + setIsSwitchChecked(false); + apiDeleteTwoFactorAuth() + .then((response) => { + updateUserData(); + }) + .catch((error) => { + setIsSwitchChecked(true); + }); + } + }; + + const handleOverlayClose = () => { + setOverlayActive(false); + setOtp(''); + setOtpLabel(defaultOtpLabel); + setOtpLabelState(null); + setIsSwitchChecked(userData.twoFactorAuth.isActive); + }; + + const handleOverlayAction = (buttonFunction: string) => { + if (!isRequestInProgress) { + setIsRequestInProgress(true); + setOtpInputInfo(''); + apiPutTwoFactorAuth({ + secret: userData.twoFactorAuth.secret, + initialCode: otp + }) + .then((response) => { + setOverlayActive(false); + setIsRequestInProgress(false); + updateUserData(); + }) + .catch((error) => { + if (error.message === FETCH_ERRORS.BAD_REQUEST) { + setOtpLabel(defaultOtpLabel); + setOtpInputInfo( + translate( + 'twoFactorAuth.activate.step3.input.label.error' + ) + ); + setOtpLabelState('invalid'); + setIsRequestInProgress(false); + setIsSwitchChecked(false); + } + }); + } + }; + + const getAuthenticatorTools = (): JSX.Element => { + const tools = [ + { + title: translate('twoFactorAuth.activate.step1.tool1'), + urlGoogle: translate( + 'twoFactorAuth.activate.step1.tool1.url.google' + ), + urlApple: translate( + 'twoFactorAuth.activate.step1.tool1.url.apple' + ) + }, + { + title: translate('twoFactorAuth.activate.step1.tool2'), + urlGoogle: translate( + 'twoFactorAuth.activate.step1.tool2.url.google' + ), + urlApple: translate( + 'twoFactorAuth.activate.step1.tool2.url.apple' + ) + } + ]; + return ( +
+ {tools.map((tool, i) => { + return ( + + ); + })} +
+ ); + }; + + const connectAuthAccount = (): JSX.Element => { + return ( +
+
+ + qr code +
+ +
+ + +
+
+ ); + }; + + const otpInputItem: InputFieldItem = { + content: otp, + id: 'otp', + infoText: otpInputInfo, + label: otpLabel, + name: 'otp', + type: 'text', + labelState: otpLabelState, + maxLength: OTP_LENGTH + }; + + const validateOtp = ( + totp + ): { validity: InputFieldLabelState; label: string } => { + if (totp.length === OTP_LENGTH) { + return { + validity: 'valid', + label: translate('twoFactorAuth.activate.step3.input.label') + }; + } else if (totp.lenght === 0) { + return { + validity: null, + label: translate('twoFactorAuth.activate.step3.input.label') + }; + } else if (totp.length < OTP_LENGTH) { + return { + validity: 'invalid', + label: translate( + 'twoFactorAuth.activate.step3.input.label.short' + ) + }; + } + }; + + const handleOtpChange = (event) => { + const validityData = validateOtp(event.target.value); + setOtpLabelState(validityData.validity); + setOtpLabel(validityData.label); + setOtp(event.target.value); + }; + + const twoFactorAuthStepsOverlay: OverlayItem[] = [ + { + headline: translate('twoFactorAuth.activate.step1.title'), + copy: translate('twoFactorAuth.activate.step1.copy'), + nestedComponent: getAuthenticatorTools(), + buttonSet: [ + { + label: translate('twoFactorAuth.overlayButton.next'), + function: OVERLAY_FUNCTIONS.NEXT_STEP, + type: BUTTON_TYPES.PRIMARY + } + ], + step: { + icon: AddIcon, + label: translate( + 'twoFactorAuth.activate.step1.visualisation.label' + ) + } + }, + { + headline: translate('twoFactorAuth.activate.step2.title'), + copy: translate('twoFactorAuth.activate.step2.copy'), + nestedComponent: connectAuthAccount(), + buttonSet: [ + { + label: translate('twoFactorAuth.overlayButton.back'), + function: OVERLAY_FUNCTIONS.PREV_STEP, + type: BUTTON_TYPES.SECONDARY + }, + { + label: translate('twoFactorAuth.overlayButton.next'), + function: OVERLAY_FUNCTIONS.NEXT_STEP, + type: BUTTON_TYPES.PRIMARY + } + ], + step: { + icon: UrlIcon, + label: translate( + 'twoFactorAuth.activate.step2.visualisation.label' + ) + } + }, + { + headline: translate('twoFactorAuth.activate.step3.title'), + copy: translate('twoFactorAuth.activate.step3.copy'), + nestedComponent: ( + + ), + buttonSet: [ + { + label: translate('twoFactorAuth.overlayButton.back'), + function: OVERLAY_FUNCTIONS.PREV_STEP, + type: BUTTON_TYPES.SECONDARY + }, + { + disabled: otpLabelState !== 'valid', + label: translate('twoFactorAuth.overlayButton.save'), + type: BUTTON_TYPES.PRIMARY + } + ], + handleOverlay: handleOverlayAction, + step: { + icon: CheckIcon, + label: translate( + 'twoFactorAuth.activate.step3.visualisation.label' + ) + } + } + ]; + + const [overlayItems, setOverlayItems] = useState( + twoFactorAuthStepsOverlay + ); + return ( +
+
+ + +
+ + {overlayActive ? ( + + + + ) : null} +
+ ); +}; diff --git a/src/components/profile/profile.styles.scss b/src/components/profile/profile.styles.scss index 84e9faf2c..e2faeedb6 100644 --- a/src/components/profile/profile.styles.scss +++ b/src/components/profile/profile.styles.scss @@ -308,7 +308,7 @@ $maxFormElementWidth: 320px; justify-content: center; margin-top: auto; padding: 0 $grid-base-two; - background-color: $dark-grey; + background-color: $profile-imprint-background-color-mobile; min-height: $imprintHeight; position: absolute; bottom: 0; @@ -486,3 +486,5 @@ $maxFormElementWidth: 320px; } } } + +@import './statistics.styles.scss'; diff --git a/src/components/profile/statistics.styles.scss b/src/components/profile/statistics.styles.scss new file mode 100644 index 000000000..40775c02f --- /dev/null +++ b/src/components/profile/statistics.styles.scss @@ -0,0 +1,62 @@ +.statistics { + &__periodSelect { + display: flex; + margin-bottom: 18px; + + > p { + margin: auto 0; + } + + .select__wrapper { + margin: 0; + } + } + + &__visuals__wrapper { + display: flex; + margin: $grid-base-four 0; + } + + &__visualization { + display: flex; + flex-direction: column; + align-items: center; + + &:first-of-type { + margin-right: $grid-base-five; + } + + span { + display: flex; + flex-grow: 1; + + svg { + width: 35px; + height: 35px; + fill: $primary; + } + + p { + color: $primary; + margin: 0 0 0 $grid-base; + font-weight: $font-weight-medium; + font-size: 30px; + line-height: 38px; + } + } + + > p { + font-weight: $font-weight-medium; + } + } + + &__download { + > p { + margin-bottom: 12px; + } + + a > svg { + margin-right: $grid-base; + } + } +} diff --git a/src/components/profile/twoFactorAuth.styles.scss b/src/components/profile/twoFactorAuth.styles.scss new file mode 100644 index 000000000..e44c5c6fe --- /dev/null +++ b/src/components/profile/twoFactorAuth.styles.scss @@ -0,0 +1,105 @@ +.twoFactorAuth { + &__switch { + display: flex; + + .text { + margin: auto 0 auto $grid-base; + } + } + + &__overlay { + .overlay__content { + @include breakpoint($fromLarge) { + height: 620px; + } + } + + .overlay__buttons { + align-self: flex-end; + margin-top: auto; + } + + .button__wrapper { + margin-top: $grid-base-three; + } + + .inputField__infoText { + color: $form-error; + text-align: left; + } + } + + &__tools { + display: grid; + grid-template-columns: auto; + + @include breakpoint($fromMedium) { + grid-template-columns: auto auto; + } + } + + &__tool { + display: grid; + margin-top: $grid-base-two; + + @include breakpoint($fromMedium) { + margin-top: 0; + } + + & > .text { + font-weight: $font-weight-bold; + text-align: left; + } + + a { + display: flex; + justify-items: start; + cursor: pointer; + margin-top: $grid-base-two; + + .text { + color: $primary; + margin: auto 0 auto $grid-base; + + &:hover { + color: $hover-primary; + } + } + } + } + + &__connect { + .text__divider { + text-transform: uppercase; + display: flex; + color: $primary; + + &::before, + &::after { + content: ''; + border-top: 1px solid $primary; + width: max-content; + margin: auto $grid-base; + flex-grow: 1; + } + } + } + + &__qrCode { + display: flex; + + .text { + margin: auto 0; + } + + img { + width: 150px; + } + } + + &__key { + .text + .text { + font-weight: $font-weight-bold; + } + } +} diff --git a/src/components/registration/Registration.tsx b/src/components/registration/Registration.tsx index 1bc3a7e8f..9629ec59a 100644 --- a/src/components/registration/Registration.tsx +++ b/src/components/registration/Registration.tsx @@ -12,18 +12,24 @@ import { apiGetConsultingType } from '../../api'; import { setValueInCookie } from '../sessionCookie/accessSessionCookie'; import '../../resources/styles/styles'; import { StageLayout } from '../stageLayout/StageLayout'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; interface RegistrationProps { handleUnmatch: Function; stageComponent: ComponentType; + legalComponent: ComponentType; } export const Registration = ({ handleUnmatch, + legalComponent, stageComponent: Stage }: RegistrationProps) => { const { consultingTypeSlug } = useParams(); - const [showWelcomeScreen, setShowWelcomeScreen] = useState(true); + const postcodeParameter = getUrlParameter('postcode'); + const [showWelcomeScreen, setShowWelcomeScreen] = useState( + !postcodeParameter + ); const [consultingType, setConsultingType] = useState< ConsultingTypeInterface | undefined >(); @@ -80,6 +86,7 @@ export const Registration = ({ return ( } @@ -94,7 +101,10 @@ export const Registration = ({ welcomeScreenConfig={consultingType.welcomeScreen} /> ) : ( - + ))} ); diff --git a/src/components/registration/RegistrationForm.tsx b/src/components/registration/RegistrationForm.tsx index a612d435c..05cde6be1 100644 --- a/src/components/registration/RegistrationForm.tsx +++ b/src/components/registration/RegistrationForm.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, ComponentType } from 'react'; import { translate } from '../../utils/translate'; import { Button, BUTTON_TYPES } from '../button/Button'; import { CheckboxItem, Checkbox } from '../checkbox/Checkbox'; @@ -31,10 +31,12 @@ import { import { FormAccordion } from '../formAccordion/FormAccordion'; import { ReactComponent as WelcomeIcon } from '../../resources/img/illustrations/willkommen.svg'; import { getUrlParameter } from '../../utils/getUrlParameter'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; import './registrationForm.styles'; interface RegistrationFormProps { consultingType: ConsultingTypeInterface; + legalComponent: ComponentType; } interface FormAccordionData { @@ -46,7 +48,10 @@ interface FormAccordionData { age?: string; } -export const RegistrationForm = ({ consultingType }: RegistrationFormProps) => { +export const RegistrationForm = ({ + consultingType, + legalComponent: LegalComponent +}: RegistrationFormProps) => { const [formAccordionData, setFormAccordionData] = useState(); const [preselectedAgencyData, setPreselectedAgencyData] = @@ -119,7 +124,13 @@ export const RegistrationForm = ({ consultingType }: RegistrationFormProps) => { name: 'dataProtectionCheckbox', labelId: 'dataProtectionLabel', label: translate('registration.dataProtection.label'), - checked: isDataProtectionSelected + checked: isDataProtectionSelected, + complexLabel: { + prefix: translate('registration.dataProtection.label.prefix'), + suffix: translate('registration.dataProtection.label.suffix'), + component: LegalComponent, + attributes: { textStyle: 'standard', hideImprint: true } + } }; const overlayItemRegistrationSuccess: OverlayItem = { diff --git a/src/components/registration/autoLogin.ts b/src/components/registration/autoLogin.ts index 017a07bba..d15314e2d 100644 --- a/src/components/registration/autoLogin.ts +++ b/src/components/registration/autoLogin.ts @@ -5,6 +5,7 @@ import { config } from '../../resources/scripts/config'; import { generateCsrfToken } from '../../utils/generateCsrfToken'; import { encodeUsername } from '../../utils/encryptionHelpers'; import { setTokens } from '../auth/auth'; +import { FETCH_ERRORS } from '../../api'; export interface LoginData { data: { @@ -21,69 +22,67 @@ export const autoLogin = (autoLoginProps: { username: string; password: string; redirect: boolean; - handleLoginError?: Function; handleLoginSuccess?: Function; + otp?: string; useOldUser?: boolean; -}) => { - const userHash = autoLoginProps.useOldUser - ? autoLoginProps.username - : encodeUsername(autoLoginProps.username); - getKeycloakAccessToken( - autoLoginProps.useOldUser ? encodeURIComponent(userHash) : userHash, - encodeURIComponent(autoLoginProps.password) - ) - .then((response) => { - setTokens( - response.access_token, - response.expires_in, - response.refresh_token, - response.refresh_expires_in - ); +}): Promise => + new Promise((resolve, reject) => { + const userHash = autoLoginProps.useOldUser + ? autoLoginProps.username + : encodeUsername(autoLoginProps.username); + getKeycloakAccessToken( + autoLoginProps.useOldUser ? encodeURIComponent(userHash) : userHash, + encodeURIComponent(autoLoginProps.password), + autoLoginProps.otp ? autoLoginProps.otp : null + ) + .then((response) => { + setTokens( + response.access_token, + response.expires_in, + response.refresh_token, + response.refresh_expires_in + ); - getRocketchatAccessToken(userHash, autoLoginProps.password) - .then((response) => { - const data = response.data; - if (data.authToken) { - setValueInCookie('rc_token', data.authToken); - } - if (data.userId) { - setValueInCookie('rc_uid', data.userId); - } + getRocketchatAccessToken(userHash, autoLoginProps.password) + .then((response) => { + const data = response.data; + if (data.authToken) { + setValueInCookie('rc_token', data.authToken); + } + if (data.userId) { + setValueInCookie('rc_uid', data.userId); + } - //generate new csrf token for current session - generateCsrfToken(true); - if (autoLoginProps.redirect) { - redirectToApp(); - } + //generate new csrf token for current session + generateCsrfToken(true); + if (autoLoginProps.redirect) { + redirectToApp(); + } - if (autoLoginProps.handleLoginSuccess) { - autoLoginProps.handleLoginSuccess(); - } - }) - .catch((error) => { - if (autoLoginProps.handleLoginError) { - autoLoginProps.handleLoginError(); - } else { - console.error(error); - } - }); - }) - .catch((error) => { - if (autoLoginProps.useOldUser) { - autoLoginProps.handleLoginError - ? autoLoginProps.handleLoginError() - : console.error(error); - } else { - autoLogin({ - username: autoLoginProps.username, - password: autoLoginProps.password, - redirect: autoLoginProps.redirect, - handleLoginError: autoLoginProps.handleLoginError, - useOldUser: true - }); - } - }); -}; + if (autoLoginProps.handleLoginSuccess) { + autoLoginProps.handleLoginSuccess(); + } + }) + .catch((error) => { + reject(error); + }); + }) + .catch((error) => { + if ( + !autoLoginProps.useOldUser && + error.message === FETCH_ERRORS.UNAUTHORIZED + ) { + autoLogin({ + username: autoLoginProps.username, + password: autoLoginProps.password, + redirect: autoLoginProps.redirect, + useOldUser: true + }).catch((error) => reject(error)); + } else { + reject(error); + } + }); + }); export const redirectToApp = () => { window.location.href = config.urls.redirectToApp; diff --git a/src/components/select/SelectDropdown.tsx b/src/components/select/SelectDropdown.tsx index 877a88885..2a9a84b6e 100644 --- a/src/components/select/SelectDropdown.tsx +++ b/src/components/select/SelectDropdown.tsx @@ -17,7 +17,7 @@ export interface SelectDropdownItem { className?: string; id: string; selectedOptions: SelectOption[]; - selectInputLabel: string; + selectInputLabel?: string; handleDropdownSelect: Function; useIconOption?: boolean; isSearchable?: boolean; @@ -149,7 +149,9 @@ export const SelectDropdown = (props: SelectDropdownItem) => { ? IconOption : components.Option, DropdownIndicator: IconDropdown, - ValueContainer: CustomValueContainer, + ValueContainer: props.selectInputLabel + ? CustomValueContainer + : components.ValueContainer, IndicatorSeparator: !props.isSearchable ? () => null : components.IndicatorSeparator diff --git a/src/components/session/SessionItemComponent.tsx b/src/components/session/SessionItemComponent.tsx index 8519056fe..e452dc148 100644 --- a/src/components/session/SessionItemComponent.tsx +++ b/src/components/session/SessionItemComponent.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState, useContext, useEffect, useMemo } from 'react'; +import { useState, useContext, useEffect, useMemo, ComponentType } from 'react'; import clsx from 'clsx'; import { typeIsEnquiry, @@ -51,6 +51,7 @@ import { ReactComponent as ArrowDoubleDownIcon } from '../../resources/img/icons import smoothScroll from './smoothScrollHelper'; import { Headline } from '../headline/Headline'; import { history } from '../app/app'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; interface SessionItemProps { currentGroupId: string; @@ -59,6 +60,7 @@ interface SessionItemProps { messages?: MessageItem[]; typingUsers: string[]; hasUserInitiatedStopOrLeaveRequest: React.MutableRefObject; + legalComponent: ComponentType; } let initMessageCount: number; @@ -326,7 +328,7 @@ export const SessionItemComponent = (props: SessionItemProps) => { const scrollBottomButtonItem: ButtonItem = { icon: , type: BUTTON_TYPES.SMALL_ICON, - smallIconBackgroundColor: 'grey' + smallIconBackgroundColor: 'alternate' }; return ( @@ -346,6 +348,7 @@ export const SessionItemComponent = (props: SessionItemProps) => { hasUserInitiatedStopOrLeaveRequest={ props.hasUserInitiatedStopOrLeaveRequest } + legalComponent={props.legalComponent} /> {!props.isAnonymousEnquiry && ( diff --git a/src/components/session/SessionView.tsx b/src/components/session/SessionView.tsx index 04a7fbe3e..0191ae8f4 100644 --- a/src/components/session/SessionView.tsx +++ b/src/components/session/SessionView.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { + ComponentType, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react'; import { history } from '../app/app'; import { useLocation } from 'react-router-dom'; import { Loading } from '../app/Loading'; @@ -29,6 +36,8 @@ import { getChatItemForSession, isGroupChatForSessionItem, prepareMessages, + getTypeOfLocation, + typeIsEnquiry, SESSION_LIST_TYPES } from './sessionHelpers'; import { JoinGroupChatView } from '../groupChat/JoinGroupChatView'; @@ -45,11 +54,18 @@ import { logout } from '../logout/logout'; import { encodeUsername, decodeUsername } from '../../utils/encryptionHelpers'; import { ReactComponent as CheckIcon } from '../../resources/img/illustrations/check.svg'; import './session.styles'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; +import { RouteComponentProps } from 'react-router-dom'; let typingTimeout; const TYPING_TIMEOUT_MS = 4000; -export const SessionView = (props) => { +interface RouterProps { + rcGroupId: string; + legalComponent: ComponentType; +} + +export const SessionView = (props: RouteComponentProps) => { const { sessionsData, setSessionsData } = useContext(SessionsDataContext); const { setAcceptedGroupId } = useContext(AcceptedGroupIdContext); const { setActiveSessionGroupId } = useContext(ActiveSessionGroupIdContext); @@ -82,7 +98,7 @@ export const SessionView = (props) => { const [isAnonymousEnquiry, setIsAnonymousEnquiry] = useState(false); const isLiveChatFinished = chatItem?.status === 3; const hasUserInitiatedStopOrLeaveRequest = useRef(false); - const isEnquiry = chatItem?.status === 1; + const isEnquiry = typeIsEnquiry(getTypeOfLocation()); const isConsultantEnquiry = isEnquiry && hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData); const [sessionListTab] = useState( @@ -325,7 +341,7 @@ export const SessionView = (props) => { } if (isGroupChat && !chatItem.subscribed) { - return ; + return ; } if (redirectToSessionsList) { @@ -350,6 +366,7 @@ export const SessionView = (props) => { messagesItem ? prepareMessages(messagesItem.messages) : null } typingUsers={typingUsers} + legalComponent={props.legalComponent} /> {isOverlayActive ? ( diff --git a/src/components/sessionCookie/getKeycloakAccessToken.ts b/src/components/sessionCookie/getKeycloakAccessToken.ts index 6a50ff16d..ae5607149 100644 --- a/src/components/sessionCookie/getKeycloakAccessToken.ts +++ b/src/components/sessionCookie/getKeycloakAccessToken.ts @@ -1,17 +1,16 @@ +import { FETCH_ERRORS } from '../../api'; import { config } from '../../resources/scripts/config'; import { LoginData } from '../registration/autoLogin'; export const getKeycloakAccessToken = ( username: string, - password: string + password: string, + otp?: string ): Promise => new Promise((resolve, reject) => { - const data = - 'username=' + - username + - '&password=' + - password + - '&client_id=app&grant_type=password'; + const data = `username=${username}&password=${password}${ + otp ? `&otp=${otp}` : `` + }&client_id=app&grant_type=password`; const url = config.endpoints.keycloakAccessToken; const req = new Request(url, { @@ -29,8 +28,10 @@ export const getKeycloakAccessToken = ( if (response.status === 200) { const data = response.json(); resolve(data); - } else if (response.status === 400 || response.status === 401) { - reject(new Error('keycloakLogin')); + } else if (response.status === 400) { + reject(new Error(FETCH_ERRORS.BAD_REQUEST)); + } else if (response.status === 401) { + reject(new Error(FETCH_ERRORS.UNAUTHORIZED)); } }) .catch((error) => { diff --git a/src/components/sessionHeader/SessionHeaderComponent.tsx b/src/components/sessionHeader/SessionHeaderComponent.tsx index c4cf8cf7a..ec96f13c9 100644 --- a/src/components/sessionHeader/SessionHeaderComponent.tsx +++ b/src/components/sessionHeader/SessionHeaderComponent.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useContext, useEffect, useState } from 'react'; +import { ComponentType, useContext, useEffect, useState } from 'react'; import clsx from 'clsx'; import { translate, @@ -38,6 +38,7 @@ import { decodeUsername } from '../../utils/encryptionHelpers'; import { ReactComponent as BackIcon } from '../../resources/img/icons/arrow-left.svg'; import './sessionHeader.styles'; import './sessionHeader.yellowTheme.styles'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; export interface SessionHeaderProps { consultantAbsent?: { @@ -46,6 +47,7 @@ export interface SessionHeaderProps { username: string; }; hasUserInitiatedStopOrLeaveRequest?: React.MutableRefObject; + legalComponent: ComponentType; } export const SessionHeaderComponent = (props: SessionHeaderProps) => { @@ -180,6 +182,7 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { props.hasUserInitiatedStopOrLeaveRequest } isAskerInfoAvailable={isAskerInfoAvailable()} + legalComponent={props.legalComponent} />
@@ -290,6 +293,7 @@ export const SessionHeaderComponent = (props: SessionHeaderProps) => { props.hasUserInitiatedStopOrLeaveRequest } isAskerInfoAvailable={isAskerInfoAvailable()} + legalComponent={props.legalComponent} />
{!activeSession?.isTeamSession || diff --git a/src/components/sessionMenu/SessionMenu.tsx b/src/components/sessionMenu/SessionMenu.tsx index 9788fd421..e137692d2 100644 --- a/src/components/sessionMenu/SessionMenu.tsx +++ b/src/components/sessionMenu/SessionMenu.tsx @@ -1,5 +1,11 @@ import * as React from 'react'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { + ComponentType, + useCallback, + useContext, + useEffect, + useState +} from 'react'; import { translate } from '../../utils/translate'; import { config } from '../../resources/scripts/config'; import { Link, Redirect, useLocation } from 'react-router-dom'; @@ -61,12 +67,14 @@ import { ReactComponent as CallOnIcon } from '../../resources/img/icons/call-on. import { ReactComponent as CameraOnIcon } from '../../resources/img/icons/camera-on.svg'; import { getVideoCallUrl } from '../../utils/videoCallHelpers'; import { removeAllCookies } from '../sessionCookie/accessSessionCookie'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; import { history } from '../app/app'; import DeleteSession from '../session/DeleteSession'; export interface SessionMenuProps { hasUserInitiatedStopOrLeaveRequest: React.MutableRefObject; isAskerInfoAvailable: boolean; + legalComponent: ComponentType; } export const SessionMenu = (props: SessionMenuProps) => { @@ -325,7 +333,7 @@ export const SessionMenu = (props: SessionMenuProps) => { }; const hasVideoCallFeatures = () => - !hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) && + hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData) && !isGroupChat && !isLiveChat && !typeIsEnquiry(getTypeOfLocation()) && @@ -588,22 +596,11 @@ export const SessionMenu = (props: SessionMenuProps) => { ) : null} - - {translate('chatFlyout.imprint')} - - - {translate('chatFlyout.dataProtection')} - +
{overlayActive ? ( diff --git a/src/components/sessionMenu/sessionMenu.styles.scss b/src/components/sessionMenu/sessionMenu.styles.scss index 0b9d8d0d5..276c636e7 100644 --- a/src/components/sessionMenu/sessionMenu.styles.scss +++ b/src/components/sessionMenu/sessionMenu.styles.scss @@ -83,6 +83,25 @@ $iconSize: 20px; visibility: visible; transition-duration: 0.25s; } + + .legalInformationLinks--menu { + display: block; + border-top: 1px solid $line-grey; + + a { + background-color: $white; + p { + color: $tertiary !important; + } + + &:hover { + background-color: $hover-select; + p { + color: $primary !important; + } + } + } + } } &__item { @@ -95,22 +114,6 @@ $iconSize: 20px; background-color: $hover-select; } - &--fixed { - background-color: $white; - color: $tertiary !important; - font-size: $font-size-tertiary; - line-height: 19px; - - &:hover { - color: $primary !important; - background-color: $hover-select; - } - } - - &--border { - border-top: 1px solid $line-grey; - } - &--mobile { display: block; } diff --git a/src/components/sessionsList/sessionsList.styles.scss b/src/components/sessionsList/sessionsList.styles.scss index 5a711c134..38243c871 100644 --- a/src/components/sessionsList/sessionsList.styles.scss +++ b/src/components/sessionsList/sessionsList.styles.scss @@ -162,12 +162,12 @@ $tabBarHeightDesktop: 53px; display: flex; width: 100%; padding: $grid-base-three; - background-color: $background-light; + background-color: $sessions-list-background-color-secondary; z-index: 5; @include breakpoint($fromLarge) { padding: 0 $grid-base-three $grid-base-three; - background-color: $background-lighter; + background-color: $sessions-list-background-color-primary; } a { diff --git a/src/components/stage/stage.tsx b/src/components/stage/stage.tsx index 95288c351..7201ee729 100644 --- a/src/components/stage/stage.tsx +++ b/src/components/stage/stage.tsx @@ -30,7 +30,7 @@ export const Stage = ({ })} >
-

{translate('app.title')}

+

{translate('app.stage.title')}

{translate('app.claim')}

diff --git a/src/components/stageLayout/StageLayout.tsx b/src/components/stageLayout/StageLayout.tsx index 57ee2f1ed..24db6417a 100644 --- a/src/components/stageLayout/StageLayout.tsx +++ b/src/components/stageLayout/StageLayout.tsx @@ -1,14 +1,15 @@ import * as React from 'react'; -import { Children, ReactNode, ReactElement } from 'react'; +import { Children, ReactNode, ReactElement, ComponentType } from 'react'; import { config } from '../../resources/scripts/config'; import { translate } from '../../utils/translate'; import { Button } from '../button/Button'; -import { LegalInformationLinks } from '../login/LegalInformationLinks'; +import { LegalInformationLinksProps } from '../login/LegalInformationLinks'; import { Text } from '../text/Text'; import './StageLayout.styles.scss'; interface StageLayoutProps { children: ReactNode; + legalComponent: ComponentType; stage: ReactNode; showLegalLinks?: boolean; showLoginLink?: boolean; @@ -18,7 +19,8 @@ export const StageLayout = ({ children, stage, showLegalLinks, - showLoginLink + showLoginLink, + legalComponent: LegalComponent }: StageLayoutProps) => { return (
@@ -28,7 +30,7 @@ export const StageLayout = ({
{children} {showLegalLinks && ( - + )}
{showLoginLink && ( diff --git a/src/components/text/Text.tsx b/src/components/text/Text.tsx index c5f2a0b1a..3224e1e6b 100644 --- a/src/components/text/Text.tsx +++ b/src/components/text/Text.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; import './text.styles'; +export type TextTypeOptions = + | 'standard' + | 'infoLargeStandard' + | 'infoLargeAlternative' + | 'infoSmall' + | 'divider'; + export interface TextProps { text: string; labelType?: LABEL_TYPES; className?: string; - type: - | 'standard' - | 'infoLargeStandard' - | 'infoLargeAlternative' - | 'infoSmall' - | 'divider'; + type: TextTypeOptions; } export enum LABEL_TYPES { diff --git a/src/components/text/text.styles.scss b/src/components/text/text.styles.scss index 3d5e85372..f2b3ef8e2 100644 --- a/src/components/text/text.styles.scss +++ b/src/components/text/text.styles.scss @@ -34,8 +34,8 @@ &__divider { font-family: $font-family-divider; font-size: $font-size-secondary; - font-weight: $text-divier-font-weight; - text-transform: $text-divier-text-transform; + font-weight: $text-divider-font-weight; + text-transform: $text-divider-text-transform; line-height: $line-height-secondary; color: $text-divider-color; letter-spacing: $text-divider-letter-spacing; diff --git a/src/globalState/interfaces/UserDataInterface.ts b/src/globalState/interfaces/UserDataInterface.ts index 6322ef049..593f5aee6 100644 --- a/src/globalState/interfaces/UserDataInterface.ts +++ b/src/globalState/interfaces/UserDataInterface.ts @@ -14,6 +14,7 @@ export interface UserDataInterface { lastName?: string; userId: string; userName: string; + twoFactorAuth?: TwoFactorAuthInterface; } export interface AgencyDataInterface { @@ -34,3 +35,10 @@ export interface ConsultingTypeDataInterface { isRegistered: boolean; sessionData: Object; } + +export interface TwoFactorAuthInterface { + isEnabled: boolean; + isActive: boolean; + secret: string; + qrCode: string; +} diff --git a/src/initApp.tsx b/src/initApp.tsx index 8cbec87a4..41a5571d8 100644 --- a/src/initApp.tsx +++ b/src/initApp.tsx @@ -1,9 +1,14 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { App } from './components/app/app'; +import { LegalInformationLinks } from './components/login/LegalInformationLinks'; import { Stage } from './components/stage/stage'; ReactDOM.render( - , + , document.getElementById('appRoot') ); diff --git a/src/initLogin.tsx b/src/initLogin.tsx deleted file mode 100644 index 100483621..000000000 --- a/src/initLogin.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { Login } from './components/login/Login'; -import { Stage } from './components/stage/stage'; - -ReactDOM.render( - , - document.getElementById('loginRoot') -); diff --git a/src/resources/img/icons/add.svg b/src/resources/img/icons/add.svg new file mode 100644 index 000000000..63fdcbd98 --- /dev/null +++ b/src/resources/img/icons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/img/icons/url.svg b/src/resources/img/icons/url.svg new file mode 100644 index 000000000..e7936fbe7 --- /dev/null +++ b/src/resources/img/icons/url.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/img/icons/verified.svg b/src/resources/img/icons/verified.svg new file mode 100644 index 000000000..c840dc66f --- /dev/null +++ b/src/resources/img/icons/verified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/resources/scripts/config.ts b/src/resources/scripts/config.ts index 7f39f9c5f..f6dabd488 100644 --- a/src/resources/scripts/config.ts +++ b/src/resources/scripts/config.ts @@ -20,7 +20,9 @@ export const config = { apiUrl + '/service/conversations/consultants/enquiries/', consultantSessions: apiUrl + '/service/users/sessions/consultants?status=2&', + consultantStatistics: apiUrl + '/service/statistics/consultant', consultantTeamSessions: apiUrl + '/service/users/sessions/teams?', + consultingTypeServiceBase: apiUrl + '/service/consultingtypes', deleteAskerAccount: apiUrl + '/service/users/account', draftMessages: apiUrl + '/service/messages/draft', email: apiUrl + '/service/users/email', @@ -41,6 +43,8 @@ export const config = { apiUrl + '/service/conversations/consultants/mymessages/', passwordReset: apiUrl + '/service/users/password/change', rejectVideoCall: apiUrl + '/service/videocalls/reject', + registerAnonymousAsker: + apiUrl + '/service/conversations/askers/anonymous/new', registerAsker: apiUrl + '/service/users/askers/new', registerAskerNewConsultingType: apiUrl + '/service/users/askers/consultingType/new', @@ -53,17 +57,16 @@ export const config = { startVideoCall: apiUrl + '/service/videocalls/new', teamSessionsBase: apiUrl + '/service/conversations/consultants/teamsessions/', - consultingTypeServiceBase: apiUrl + '/service/consultingtypes', + twoFactorAuth: apiUrl + '/service/users/twoFactorAuth', userData: apiUrl + '/service/users/data', updateMonitoring: apiUrl + '/service/users/sessions/monitoring', - userSessionsListView: '/sessions/user/view', - registerAnonymousAsker: - apiUrl + '/service/conversations/askers/anonymous/new' + userSessionsListView: '/sessions/user/view' }, urls: { loginRedirectToRegistrationOverview: 'https://www.caritas.de/onlineberatung', - toLogin: uiUrl + '/', + toLogin: uiUrl + '/login', + toEntry: uiUrl + '/', redirectToApp: uiUrl + '/' + APP_PATH, home: 'https://www.caritas.de', finishedAnonymousChatRedirect: @@ -73,14 +76,6 @@ export const config = { 'https://www.caritas.de/hilfeundberatung/onlineberatung/datenschutz', error500: uiUrl + '/error.500.html', error401: uiUrl + '/error.401.html', - error404: uiUrl + '/error.404.html', - registrationDisabilityPostcodeFallback: - 'https://www.caritas.de/hilfeundberatung/onlineberatung/behinderung-und-psychische-erkrankung/adressen', - registrationMigrationPostcodeFallback: - 'https://www.caritas.de/hilfeundberatung/onlineberatung/migration/adressen', - registrationHospicePostcodeFallback: - 'https://www.caritas.de/hilfeundberatung/onlineberatung/hospiz-palliativ/adressen', - registrationMenPostcodeFallback: - 'https://www.skmev.de/beratung-hilfe/jungen-und-maennerarbeit/' + error404: uiUrl + '/error.404.html' } }; diff --git a/src/resources/scripts/i18n/de.ts b/src/resources/scripts/i18n/de.ts index e509c04e3..dc99d76dd 100644 --- a/src/resources/scripts/i18n/de.ts +++ b/src/resources/scripts/i18n/de.ts @@ -15,12 +15,14 @@ import login from './de/login'; import message from './de/message'; import monitoring from './de/monitoring'; import navigation from './de/navigation'; +import overlay from './de/overlay'; import profile from './de/profile'; import registration from './de/registration'; import session from './de/session'; import user from './de/user'; import sessionList from './de/sessionList'; import statusOverlay from './de/statusOverlay'; +import twoFactorAuth from './de/twoFactorAuth'; import typingIndicator from './de/typingIndicator'; import userProfile from './de/userProfile'; import videoCall from './de/videoCall'; @@ -43,11 +45,13 @@ const de = { message, monitoring, navigation, + overlay, profile, registration, session, sessionList, statusOverlay, + twoFactorAuth, typingIndicator, user, userProfile, diff --git a/src/resources/scripts/i18n/de/anonymous.ts b/src/resources/scripts/i18n/de/anonymous.ts index e2bb046f3..34ea1e448 100644 --- a/src/resources/scripts/i18n/de/anonymous.ts +++ b/src/resources/scripts/i18n/de/anonymous.ts @@ -39,7 +39,7 @@ const anonymous = { 'Um Ihre Anonymität zu schützen, löschen wir Ihre Nachrichten spätestens 48 Stunden nachdem der Chat beendet wurde.', 'waitingroom.redirect.title': 'Sie wollen nicht warten?', 'waitingroom.redirect.subline': - 'Registrieren Sie sich und hinterlassen Sie uns Ihre Nachricht. Wir melden uns innerhalb von zwei Werktagen bei Ihnen.', + 'Registrieren Sie sich und hinterlassen Sie uns Ihre Nachricht. Wir melden uns innerhalb von 2 Werktagen bei Ihnen.', 'waitingroom.redirect.button': 'Zur Registrierung', 'waitingroom.overlay.acceptance.headline': 'Herzlich Willkommen!', 'waitingroom.overlay.acceptance.copy': @@ -47,7 +47,7 @@ const anonymous = { 'waitingroom.overlay.acceptance.button': 'Jetzt chatten', 'waitingroom.overlay.rejection.headline': 'Chat-Zeit beendet.', 'waitingroom.overlay.rejection.copy': - 'Leider konnten wir innerhalb der Chat-Zeit nicht auf Ihr Anliegen eingehen. Registrieren Sie sich und hinterlassen Sie uns Ihre Nachricht. Wir melden uns innerhalb von zwei Werktagen bei Ihnen.', + 'Leider konnten wir innerhalb der Chat-Zeit nicht auf Ihr Anliegen eingehen. Registrieren Sie sich und hinterlassen Sie uns Ihre Nachricht. Wir melden uns innerhalb von 2 Werktagen bei Ihnen.', 'waitingroom.overlay.rejection.button': 'Zur Registrierung' }; diff --git a/src/resources/scripts/i18n/de/app.ts b/src/resources/scripts/i18n/de/app.ts index 5a56b38e2..aeac22472 100644 --- a/src/resources/scripts/i18n/de/app.ts +++ b/src/resources/scripts/i18n/de/app.ts @@ -1,8 +1,9 @@ const app = { - title: 'Beratung & Hilfe', - claim: 'Online. Anonym. Sicher.', - save: 'Speichern', - logout: 'Abmelden' + 'title': 'Beratung & Hilfe', + 'claim': 'Online. Anonym. Sicher.', + 'save': 'Speichern', + 'stage.title': 'Beratung & Hilfe', + 'logout': 'Abmelden' }; export default app; diff --git a/src/resources/scripts/i18n/de/enquiryInformal.ts b/src/resources/scripts/i18n/de/enquiryInformal.ts index a00820243..65ea7fbe2 100644 --- a/src/resources/scripts/i18n/de/enquiryInformal.ts +++ b/src/resources/scripts/i18n/de/enquiryInformal.ts @@ -8,7 +8,7 @@ const enquiryInformal = { '
  • Was ist passiert?
  • Wie ist Deine aktuelle Situation?
  • Was beschäftigt Dich?
  • Hast Du eine bestimmte Frage oder weißt Du vielleicht selbst noch nicht so genau was Dir helfen könnte?
', 'write.overlayHeadline': 'Vielen Dank für Deine Nachricht!', 'write.overlayCopy': - 'Innerhalb von zwei Werktagen erhältst Du eine Antwort von uns.' + 'Innerhalb von 2 Werktagen erhältst Du eine Antwort von uns.' }; export default enquiryInformal; diff --git a/src/resources/scripts/i18n/de/login.ts b/src/resources/scripts/i18n/de/login.ts index 59813762b..a3802f0a7 100644 --- a/src/resources/scripts/i18n/de/login.ts +++ b/src/resources/scripts/i18n/de/login.ts @@ -2,8 +2,12 @@ const login = { 'headline': 'Login', 'user.label': 'Benutzername/E-Mail', 'password.label': 'Passwort', - 'warning.failed': + 'warning.failed.unauthorized': 'Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut.', + 'warning.failed.unauthorized.otp': + 'Ihre Zugangsdaten sind nicht korrekt. Bitte versuchen Sie es erneut.', + 'warning.failed.otp.missing': + 'Bitte geben Sie den Code aus Ihrer App für die 2-Faktor-Authentifizierung ein.', 'button.label': 'Anmelden', 'resetPasswort.label': 'Passwort vergessen?', 'register.infoText.title': 'Noch nicht registriert?', diff --git a/src/resources/scripts/i18n/de/overlay.ts b/src/resources/scripts/i18n/de/overlay.ts new file mode 100644 index 000000000..c2e36d49d --- /dev/null +++ b/src/resources/scripts/i18n/de/overlay.ts @@ -0,0 +1,5 @@ +const overlay = { + 'step.headline.prefix': '. Schritt | ' +}; + +export default overlay; diff --git a/src/resources/scripts/i18n/de/profile.ts b/src/resources/scripts/i18n/de/profile.ts index bbd441f8e..8821f75e7 100644 --- a/src/resources/scripts/i18n/de/profile.ts +++ b/src/resources/scripts/i18n/de/profile.ts @@ -60,7 +60,26 @@ const profile = { 'externalRegistration.submit': 'Jetzt wechseln', 'externalRegistration.cancel': 'Abbrechen', 'footer.imprint': 'Impressum', - 'footer.dataprotection': 'Datenschutz' + 'footer.dataprotection': 'Datenschutz', + 'statistics.title': 'Meine Statistik', + 'statistics.period.prefix': 'Ihre Zahlen des', + 'statistics.period.lastMonth': 'letzten Monats', + 'statistics.period.currentMonth': 'aktuellen Monats', + 'statistics.period.currentYear': 'aktuellen Jahres', + 'statistics.period.lastYear': 'vergangenen Jahres', + 'statistics.period.display.default': 'DD.MM.JJJJ - DD.MM.JJJJ', + 'statistics.period.display.prefix': 'Im Zeitraum vom ', + 'statistics.period.display.suffix': ' haben Sie:', + 'statistics.complete.title': + 'Ihre Statistik über Ihren gesamten Beratungszeitraum können Sie hier herunterladen:', + 'statistics.complete.filename': 'Gesamtstatistik Online-Beratung.csv', + 'statistics.complete.download.label': 'Download Excel Datei', + 'statistics.csvHeader.numberOfAssignedSessions': 'Beratungen angenommen', + 'statistics.csvHeader.numberOfSentMessages': 'Nachrichten geschrieben', + 'statistics.csvHeader.numberOfSessionsWhereConsultantWasActive': + 'Aktive Beratungen', + 'statistics.csvHeader.videoCallDuration': + 'Dauer von Videoanrufen in Minuten' }; export default profile; diff --git a/src/resources/scripts/i18n/de/profileInformal.ts b/src/resources/scripts/i18n/de/profileInformal.ts index 122c6abf1..aaa48979f 100644 --- a/src/resources/scripts/i18n/de/profileInformal.ts +++ b/src/resources/scripts/i18n/de/profileInformal.ts @@ -24,7 +24,11 @@ const profileInformal = { 'Deine gewählte Beratungsstelle nutzt eine andere Anwendung für die Beratung', 'externalRegistration.copy.start': 'Möchtest Du für „', 'externalRegistration.copy.end': - '“ zu der anderen Anwendung wechseln und dich dort registrieren? Deine bisherigen Beratungs- und Hilfethemen findest Du weiterhin hier.' + '“ zu der anderen Anwendung wechseln und dich dort registrieren? Deine bisherigen Beratungs- und Hilfethemen findest Du weiterhin hier.', + 'statistics.period.prefix': 'Deine Zahlen des', + 'statistics.period.display.suffix': ' hast Du:', + 'statistics.complete.title': + 'Deine Statistik über Deinen gesamten Beratungszeitraum kannst Du hier herunterladen:' }; export default profileInformal; diff --git a/src/resources/scripts/i18n/de/registration.ts b/src/resources/scripts/i18n/de/registration.ts index 18966afd4..b26018096 100644 --- a/src/resources/scripts/i18n/de/registration.ts +++ b/src/resources/scripts/i18n/de/registration.ts @@ -26,7 +26,7 @@ const registration = { 'Sie gegebenfalls auch vor Ort beraten kann.', 'agencySelection.postcode.label': 'Ihre Postleitzahl', 'agencySelection.postcode.unavailable': - 'Momentan haben wir leider noch keine Online-Beratungsstelle in Ihrer Nähe. Auf unserer Webseite finden Sie Beratungsstellen für Ihr Anliegen.', + 'Momentan haben wir leider noch keine Online-Beratungsstelle in Ihrer Nähe. Auf unserer Webseite finden Sie Beratungsstellen vor Ort für Ihr Anliegen.', 'agencySelection.postcode.search': 'Zur Beratungsstellensuche', 'agencyPreselected.headline': 'Bitte geben Sie Ihre Postleitzahl an', 'agencyPreselected.intro.overline': @@ -72,6 +72,9 @@ const registration = { 'state.options.16': 'Thüringen', 'dataProtection.label': 'Ich habe die Datenschutzerklärung zur Kenntnis genommen. Für Authentifizierung und Navigation verwendet diese Webseite Cookies. Damit erkläre ich mich einverstanden.', + 'dataProtection.label.prefix': 'Ich habe die ', + 'dataProtection.label.suffix': + ' zur Kenntnis genommen. Für Authentifizierung und Navigation verwendet diese Webseite Cookies. Damit erkläre ich mich einverstanden.', 'submitButton.label': 'Registrieren', 'overlay.success.headline': 'Herzlich willkommen
bei der Beratung & Hilfe der Caritas.', @@ -87,7 +90,7 @@ const registration = { 'Sie schicken Ihre Nachricht an eine lokale Beratungsstelle', 'welcomeScreen.info3.title': 'Persönliche und professionelle Beratung', 'welcomeScreen.info3.text': - 'Innerhalb von zwei Werktagen bekommen Sie eine Antwort', + 'Innerhalb von 2 Werktagen bekommen Sie eine Antwort', 'welcomeScreen.info4.title': 'Anonym und kostenfrei', 'welcomeScreen.info4.text': 'Sie bleiben anonym und erhalten kostenfreie Beratung und Hilfe', diff --git a/src/resources/scripts/i18n/de/registrationInformal.ts b/src/resources/scripts/i18n/de/registrationInformal.ts index 2a74af1f0..55d531611 100644 --- a/src/resources/scripts/i18n/de/registrationInformal.ts +++ b/src/resources/scripts/i18n/de/registrationInformal.ts @@ -14,7 +14,7 @@ const registrationInformal = { 'Dich gegebenfalls auch vor Ort beraten kann.', 'agencySelection.postcode.label': 'Deine Postleitzahl', 'agencySelection.postcode.unavailable': - 'Momentan haben wir leider noch keine Online-Beratungsstelle in Deiner Nähe. Auf unserer Webseite findest Du Beratungsstellen für Dein Anliegen.', + 'Momentan haben wir leider noch keine Online-Beratungsstelle in Deiner Nähe. Auf unserer Webseite findest Du Beratungsstellen vor Ort für Dein Anliegen.', 'agencyPreselected.headline': 'Bitte gib Deine Postleitzahl an', 'agencyPreselected.intro.overline': 'Warum benötigen wir Deine Postleitzahl?', @@ -34,7 +34,7 @@ const registrationInformal = { 'welcomeScreen.info2.text': 'Schicke Deine Nachricht an eine lokale Beratungsstelle', 'welcomeScreen.info3.text': - 'Innerhalb von zwei Werktagen bekommst Du eine Antwort' + 'Innerhalb von 2 Werktagen bekommst Du eine Antwort' }; export default registrationInformal; diff --git a/src/resources/scripts/i18n/de/twoFactorAuth.ts b/src/resources/scripts/i18n/de/twoFactorAuth.ts new file mode 100644 index 000000000..ddcffdcff --- /dev/null +++ b/src/resources/scripts/i18n/de/twoFactorAuth.ts @@ -0,0 +1,45 @@ +const twoFactorAuth = { + 'title': '2-Faktor-Authentifizierung', + 'subtitle': + 'Nutzen Sie eine weitere App für die Anmeldung mit Ihrem Diakonie Konto. Dadurch ist Ihr Konto sicherer vor einem möglichen unbefugtem Zugriff.', + 'switch.active.label': '2-Faktor-Authentifizierung aktiviert', + 'switch.deactive.label': '2-Faktor-Authentifizierung deaktiviert', + 'activate.step1.title': 'Installieren Sie sich die App', + 'activate.step1.copy': + 'Installieren Sie sich FreeOTP oder Google Authentificator auf Ihrem Smartphone oder Tablet. Beide Apps sind im Google Play oder Apple App Store verfügbar.', + 'activate.step1.visualisation.label': 'Installation', + 'activate.step1.tool1': 'FreeOTP App:', + 'activate.step1.tool2': 'Google Authenticator App:', + 'activate.step1.tool1.url.google': + 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', + 'activate.step1.tool1.url.apple': + 'https://apps.apple.com/de/app/freeotp-authenticator/id872559395', + 'activate.step1.tool2.url.google': + 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', + 'activate.step1.tool2.url.apple': + 'https://apps.apple.com/de/app/google-authenticator/id388497605', + 'activate.step1.download.google': 'Download im Google Play Store', + 'activate.step1.download.apple': 'Download im Apple App Store', + 'activate.step2.title': 'Verknüpfen Sie die App und Ihren Account', + 'activate.step2.copy': + 'Sie haben zwei Möglichkeiten die App mit Ihrem Account zu verknüpfen:', + 'activate.step2.visualisation.label': 'Verknüpfung', + 'activate.step2.connect.qrCode': + 'Öffnen Sie die App und scannen Sie den folgenden QR-Code:', + 'activate.step2.connect.divider': 'oder', + 'activate.step2.connect.key': + 'Geben Sie den folgenden 32-stelligen Schlüssel ein:', + 'activate.step3.title': 'Einmal-Code eingeben', + 'activate.step3.copy': + 'Geben Sie den Einmal-Code ein, der von der App generiert wird und klicken Sie auf „Speichern“, um die Einrichtung abzuschließen.', + 'activate.step3.visualisation.label': 'Bestätigung', + 'activate.step3.input.label': 'Einmal-Code', + 'activate.step3.input.label.short': 'Der eingegebene Code ist zu kurz.', + 'activate.step3.input.label.error': + 'Die Authentifizierung fehlgeschlagen. Bitte wiederholen Sie den Vorgang.', + 'overlayButton.next': 'Weiter', + 'overlayButton.back': 'Zurück', + 'overlayButton.save': 'Speichern' +}; + +export default twoFactorAuth; diff --git a/src/resources/scripts/i18n/de/twoFactorAuthInformal.ts b/src/resources/scripts/i18n/de/twoFactorAuthInformal.ts new file mode 100644 index 000000000..774a3f282 --- /dev/null +++ b/src/resources/scripts/i18n/de/twoFactorAuthInformal.ts @@ -0,0 +1,20 @@ +const twoFactorAuthInformal = { + 'subtitle': + 'Nutze eine weitere App für die Anmeldung mit Deinem Diakonie Konto. Dadurch ist Dein Konto sicherer vor einem möglichen unbefugtem Zugriff.', + 'activate.step1.title': 'Installiere Dir die App', + 'activate.step1.copy': + 'Installiere Dir FreeOTP oder Google Authentificator auf Deinem Smartphone oder Tablet. Beide Apps sind im Google Play oder Apple App Store verfügbar.', + 'activate.step2.title': 'Verknüpfe die App und Deinen Account', + 'activate.step2.copy': + 'Du hast zwei Möglichkeiten die App mit Deinem Account zu verknüpfen:', + 'activate.step2.connect.qrCode': + 'Öffne die App und scanne den folgenden QR-Code:', + 'activate.step2.connect.key': + 'Gebe den folgenden 32-stelligen Schlüssel ein:', + 'activate.step3.copy': + 'Gib den Einmal-Code ein, der von der App generiert wird und klicke auf „Speichern“, um die Einrichtung abzuschließen.', + 'activate.step3.input.label.error': + 'Die Authentifizierung fehlgeschlagen. Bitte wiederhole den Vorgang.' +}; + +export default twoFactorAuthInformal; diff --git a/src/resources/scripts/i18n/deInformal.ts b/src/resources/scripts/i18n/deInformal.ts index faf34767d..1831f7e90 100644 --- a/src/resources/scripts/i18n/deInformal.ts +++ b/src/resources/scripts/i18n/deInformal.ts @@ -24,6 +24,8 @@ import sessionList from './de/sessionList'; import sessionListInformal from './de/sessionListInformal'; import statusOverlay from './de/statusOverlay'; import statusOverlayInformal from './de/statusOverlayInformal'; +import twoFactorAuth from './de/twoFactorAuth'; +import twoFactorAuthInformal from './de/twoFactorAuthInformal'; import videoCall from './de/videoCall'; import videoCallInformal from './de/videoCallInformal'; import de from './de'; @@ -43,6 +45,7 @@ const informalLocale = { session: { ...session, ...sessionInformal }, sessionList: { ...sessionList, ...sessionListInformal }, statusOverlay: { ...statusOverlay, ...statusOverlayInformal }, + twoFactorAuth: { ...twoFactorAuth, ...twoFactorAuthInformal }, videoCall: { ...videoCall, ...videoCallInformal } }; diff --git a/src/resources/styles/settings.scss b/src/resources/styles/settings.scss index 28aed2290..7b92daa2d 100644 --- a/src/resources/styles/settings.scss +++ b/src/resources/styles/settings.scss @@ -157,6 +157,7 @@ $button-box-shadow-default: 0 6px 0 0 rgba(0, 0, 0, 0.1); $button-box-shadow-grey: 0 3px 0 0 rgba(0, 0, 0, 0.1); $button-small-icon-background-color-default: $light-grey; $button-small-icon-border-radius: none; +$button-small-icon-alternate-background-color: $light-grey; $checkbox-border-radius: 4px; $component-max-width: 1200px; $content-max-width: $xlarge; @@ -183,13 +184,12 @@ $login-button-width: auto; $login-text-align: center; $max-input-width: 320px; $message-background: #d64b49; -$message-item-divider-color: inherit; -$message-item-divider-font-weight: $font-weight-regular; -$message-item-divider-letter-spacing: 0; -$message-item-divider-line-color: $form-disabled; $message-submit-interface-textarea-background-color: $dark-grey; $overlay-text-align: center; +$overlay-input-field-margin: $grid-base-three auto; +$overlay-button-margin: $grid-base-two auto 0 -$grid-base; $profile-divider-text-align: center; +$profile-imprint-background-color-mobile: $dark-grey; $registration-form-max-width: 400px; $registration-text-align: center; $select-dropdown-border-radius: 25px; @@ -205,7 +205,7 @@ $sessions-list-background-color-primary: $background-lighter; $sessions-list-background-color-secondary: $background-light; $text-divider-color: $light-grey; $text-divider-letter-spacing: 0; -$text-divier-font-weight: $font-weight-regular; -$text-divier-text-transform: none; +$text-divider-font-weight: $font-weight-regular; +$text-divider-text-transform: none; $upload-progress: #80dd92; $welcome-screen-icon-background: $background-accent;