From f61a052bf512185fb2dc92584d56e34ab2507187 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 16 Jan 2019 23:01:34 +0100 Subject: [PATCH] Authentication workflow is complete. --- client-react/package-lock.json | 523 +++++++++++++++++- client-react/package.json | 2 + .../src/assets/images/applestore-badge.svg | 129 +++++ .../src/assets/images/googleplay-badge.svg | 429 ++++++++++++++ .../FormNotification/FormNotification.ts | 2 - .../ForgotPasswordView/ForgotPasswordView.ts | 4 +- .../OneTimePasswordRegistrationView.ts | 44 +- .../ResetPasswordView/ResetPasswordView.ts | 4 +- .../SecondFactorView/SecondFactorView.ts | 29 +- .../SecurityKeyRegistrationView.ts | 4 + .../CircleLoader/CircleLoader.module.scss | 148 +++++ .../components/CircleLoader/CircleLoader.tsx | 44 ++ .../StateSynchronizer/StateSynchronizer.ts | 8 +- .../layouts/PortalLayout/PortalLayout.ts | 2 +- .../views/FirstFactorView/FirstFactorView.ts | 20 +- .../OneTimePasswordRegistrationView.ts | 46 +- .../SecondFactorView/SecondFactorView.ts | 40 +- .../SecurityKeyRegistrationView.ts | 32 +- .../Portal/{ => FirstFactor}/actions.ts | 15 +- .../Portal/{ => FirstFactor}/reducer.ts | 29 +- .../OneTimePasswordRegistration/actions.ts | 13 + .../OneTimePasswordRegistration/reducer.ts | 50 ++ .../reducers/Portal/SecondFactor/actions.ts | 26 + .../reducers/Portal/SecondFactor/reducer.ts | 75 +++ .../Portal/SecurityKeyRegistration/actions.ts | 8 + .../Portal/SecurityKeyRegistration/reducer.ts | 36 ++ client-react/src/reducers/Portal/index.ts | 13 + client-react/src/reducers/constants.ts | 17 + client-react/src/reducers/index.ts | 5 +- .../views/FirstFactorView/FirstFactorView.tsx | 12 +- .../ForgotPasswordView/ForgotPasswordView.tsx | 2 +- .../OneTimePasswordRegistrationView.tsx | 135 +++-- .../OneTimePasswordRegistrationView/Secret.ts | 5 + .../ResetPasswordView/ResetPasswordView.tsx | 2 + .../SecondFactorView/SecondFactorView.tsx | 90 +-- .../SecurityKeyRegistrationView.tsx | 61 +- 36 files changed, 1887 insertions(+), 217 deletions(-) create mode 100644 client-react/src/assets/images/applestore-badge.svg create mode 100644 client-react/src/assets/images/googleplay-badge.svg create mode 100644 client-react/src/components/CircleLoader/CircleLoader.module.scss create mode 100644 client-react/src/components/CircleLoader/CircleLoader.tsx rename client-react/src/reducers/Portal/{ => FirstFactor}/actions.ts (69%) rename client-react/src/reducers/Portal/{ => FirstFactor}/reducer.ts (72%) create mode 100644 client-react/src/reducers/Portal/OneTimePasswordRegistration/actions.ts create mode 100644 client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts create mode 100644 client-react/src/reducers/Portal/SecondFactor/actions.ts create mode 100644 client-react/src/reducers/Portal/SecondFactor/reducer.ts create mode 100644 client-react/src/reducers/Portal/SecurityKeyRegistration/actions.ts create mode 100644 client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts create mode 100644 client-react/src/reducers/Portal/index.ts create mode 100644 client-react/src/views/OneTimePasswordRegistrationView/Secret.ts diff --git a/client-react/package-lock.json b/client-react/package-lock.json index 2c93ee2e..b56ad64d 100644 --- a/client-react/package-lock.json +++ b/client-react/package-lock.json @@ -955,6 +955,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" }, + "@types/node-sass": { + "version": "3.10.32", + "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-3.10.32.tgz", + "integrity": "sha1-spbM5xRP+rd7hAkMqtTx5Lvqjgk=", + "requires": { + "@types/node": "*" + } + }, "@types/prop-types": { "version": "15.5.8", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", @@ -1218,6 +1226,11 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -1297,6 +1310,11 @@ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, "ansi-colors": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", @@ -1605,6 +1623,15 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1650,6 +1677,11 @@ "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=" }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -1776,6 +1808,11 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" + }, "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", @@ -2451,6 +2488,14 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==" }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, "bluebird": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", @@ -2795,6 +2840,22 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + } + } + }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -3244,6 +3305,11 @@ "date-now": "^0.1.4" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, "constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -3804,6 +3870,14 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.0.tgz", "integrity": "sha512-by8hi8BlLbowQq0qtkx54d9aN73R9oUW20HISpka5kmgsR9F7nnxgfsemuR2sdCKZh+CDNf5egW9UZMm4mgJRg==" }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -4002,6 +4076,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -5878,11 +5957,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5895,15 +5976,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6006,7 +6090,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6016,6 +6101,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6028,17 +6114,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6055,6 +6144,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6127,7 +6217,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6137,6 +6228,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6242,6 +6334,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6305,6 +6398,17 @@ } } }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6315,6 +6419,49 @@ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "requires": { + "globule": "^1.0.0" + } + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -6325,6 +6472,11 @@ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==" }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -6431,6 +6583,16 @@ } } }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -6573,6 +6735,11 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -7252,6 +7419,11 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=" + }, "indefinite-observable": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-1.0.2.tgz", @@ -7260,6 +7432,14 @@ "symbol-observable": "1.2.0" } }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, "indexes-of": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", @@ -8327,6 +8507,11 @@ "topo": "2.x.x" } }, + "js-base64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz", + "integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==" + }, "js-levenshtein": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.5.tgz", @@ -8712,11 +8897,21 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -8727,6 +8922,11 @@ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, + "lodash.mergewith": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", + "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8772,6 +8972,15 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -8827,6 +9036,11 @@ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -8877,6 +9091,23 @@ "readable-stream": "^2.0.1" } }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, "merge": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", @@ -9107,8 +9338,7 @@ "nan": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", - "optional": true + "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" }, "nanomatch": { "version": "1.2.13", @@ -9187,6 +9417,32 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==" }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9248,6 +9504,73 @@ "semver": "^5.3.0" } }, + "node-sass": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash.assign": "^4.2.0", + "lodash.clonedeep": "^4.3.2", + "lodash.mergewith": "^4.6.0", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.10.0", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -9290,6 +9613,17 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -9527,6 +9861,15 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -14197,6 +14540,15 @@ "minimatch": "3.0.4" } }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, "redux": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", @@ -14866,6 +15218,93 @@ } } }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "requires": { + "camelcase": "^3.0.0" + } + } + } + }, "sass-loader": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", @@ -14959,6 +15398,25 @@ "ajv-keywords": "^3.1.0" } }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -15523,6 +15981,14 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "requires": { + "readable-stream": "^2.0.1" + } + }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", @@ -15664,6 +16130,14 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -15811,6 +16285,16 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==" }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, "terser": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/terser/-/terser-3.14.1.tgz", @@ -16060,11 +16544,24 @@ "punycode": "^2.1.0" } }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "requires": { + "glob": "^7.1.2" + } + }, "tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -17237,6 +17734,14 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/client-react/package.json b/client-react/package.json index 8f5933aa..60e321b1 100644 --- a/client-react/package.json +++ b/client-react/package.json @@ -8,6 +8,7 @@ "@types/classnames": "^2.2.7", "@types/jss": "^9.5.7", "@types/node": "^10.12.2", + "@types/node-sass": "^3.10.32", "@types/qrcode.react": "^0.8.1", "@types/query-string": "^6.2.0", "@types/react": "^16.4.18", @@ -18,6 +19,7 @@ "await-to-js": "^2.1.1", "classnames": "^2.2.6", "jss": "^9.8.7", + "node-sass": "^4.11.0", "qrcode.react": "^0.9.2", "query-string": "^6.2.0", "react": "^16.6.0", diff --git a/client-react/src/assets/images/applestore-badge.svg b/client-react/src/assets/images/applestore-badge.svg new file mode 100644 index 00000000..ac111e59 --- /dev/null +++ b/client-react/src/assets/images/applestore-badge.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client-react/src/assets/images/googleplay-badge.svg b/client-react/src/assets/images/googleplay-badge.svg new file mode 100644 index 00000000..9e33e3aa --- /dev/null +++ b/client-react/src/assets/images/googleplay-badge.svg @@ -0,0 +1,429 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/client-react/src/assets/jss/components/FormNotification/FormNotification.ts b/client-react/src/assets/jss/components/FormNotification/FormNotification.ts index 3a715e37..db8d549e 100644 --- a/client-react/src/assets/jss/components/FormNotification/FormNotification.ts +++ b/client-react/src/assets/jss/components/FormNotification/FormNotification.ts @@ -3,8 +3,6 @@ import { createStyles, Theme } from "@material-ui/core"; const styles = createStyles((theme: Theme) => ({ messageOuter: { position: 'relative', - paddingTop: theme.spacing.unit * 2, - paddingBottom: theme.spacing.unit, }, messageInner: { width: '100%', diff --git a/client-react/src/assets/jss/views/ForgotPasswordView/ForgotPasswordView.ts b/client-react/src/assets/jss/views/ForgotPasswordView/ForgotPasswordView.ts index 08ba8e15..330c78da 100644 --- a/client-react/src/assets/jss/views/ForgotPasswordView/ForgotPasswordView.ts +++ b/client-react/src/assets/jss/views/ForgotPasswordView/ForgotPasswordView.ts @@ -2,13 +2,13 @@ import { createStyles, Theme } from "@material-ui/core"; const styles = createStyles((theme: Theme) => ({ form: { - padding: '20px 0px', + paddingTop: theme.spacing.unit * 2, }, field: { width: '100%', }, button: { - marginTop: '20px', + marginTop: theme.spacing.unit * 2, width: '100%', } })); diff --git a/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts b/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts index d955e688..10b4ce28 100644 --- a/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts +++ b/client-react/src/assets/jss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts @@ -1,14 +1,54 @@ import { createStyles, Theme } from "@material-ui/core"; +const borderColor = '#e0e0e0'; + const styles = createStyles((theme: Theme) => ({ secretContainer: { width: '100%', + border: '1px solid ' + borderColor, + marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 2, }, qrcodeContainer: { textAlign: 'center', + padding: theme.spacing.unit * 2, }, - textField: { - width: '100%', + base32Container: { + textAlign: 'center', + borderTop: '1px solid ' + borderColor, + padding: theme.spacing.unit, + wordWrap: 'break-word', + }, + text: { + textAlign: 'center', + }, + needGoogleAuthenticator: { + textAlign: 'center', + marginTop: theme.spacing.unit * 2, + }, + needGoogleAuthenticatorText: { + fontSize: theme.typography.fontSize * 0.8, + }, + store: { + width: '100px', + marginTop: theme.spacing.unit * 0.5, + marginLeft: theme.spacing.unit * 0.5, + marginRight: theme.spacing.unit * 0.5, + }, + buttonContainer: { + textAlign: 'center', + paddingTop: theme.spacing.unit * 2, + }, + progressContainer: { + textAlign: 'center', + paddingTop: theme.spacing.unit * 2, + }, + button: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + }, + loginButtonContainer: { + textAlign: 'center', }, })); diff --git a/client-react/src/assets/jss/views/ResetPasswordView/ResetPasswordView.ts b/client-react/src/assets/jss/views/ResetPasswordView/ResetPasswordView.ts index d6b6bc34..a147fc88 100644 --- a/client-react/src/assets/jss/views/ResetPasswordView/ResetPasswordView.ts +++ b/client-react/src/assets/jss/views/ResetPasswordView/ResetPasswordView.ts @@ -2,15 +2,13 @@ import { createStyles, Theme } from "@material-ui/core"; const styles = createStyles((theme: Theme) => ({ form: { - paddingTop: theme.spacing.unit * 2, - paddingBottom: theme.spacing.unit * 2, + marginTop: theme.spacing.unit * 2, }, field: { width: '100%', marginBottom: theme.spacing.unit * 2, }, button: { - marginTop: theme.spacing.unit * 2, width: '100%', } })); diff --git a/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts b/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts index 088735f8..f90a33c1 100644 --- a/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts +++ b/client-react/src/assets/jss/views/SecondFactorView/SecondFactorView.ts @@ -25,12 +25,25 @@ const styles = createStyles((theme: Theme) => ({ body: { paddingTop: theme.spacing.unit * 2, paddingBottom: theme.spacing.unit * 2, - paddingLeft: theme.spacing.unit, - paddingRight: theme.spacing.unit, + paddingLeft: theme.spacing.unit * 2, + paddingRight: theme.spacing.unit * 2, border: '1px solid #e0e0e0', borderRadius: '2px', textAlign: 'justify', }, + methodName: { + fontSize: theme.typography.fontSize * 1.2, + fontWeight: 'bold', + marginBottom: theme.spacing.unit, + }, + methodU2f: { + borderBottom: '1px solid #e0e0e0', + padding: theme.spacing.unit, + }, + methodTotp: { + padding: theme.spacing.unit, + paddingTop: theme.spacing.unit * 2, + }, image: { width: '120px', }, @@ -39,19 +52,17 @@ const styles = createStyles((theme: Theme) => ({ marginTop: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 2, }, - footer: { - paddingTop: theme.spacing.unit, - fontSize: theme.typography.fontSize * 0.9, - }, - registerDevice: { - float: 'right', + registerDeviceContainer: { + textAlign: 'right', + fontSize: theme.typography.fontSize * 0.8, }, + registerDevice: {}, totpField: { marginTop: theme.spacing.unit * 2, - marginBottom: theme.spacing.unit * 2, width: '100%', }, totpButton: { + marginTop: theme.spacing.unit * 2, width: '100%', } })); diff --git a/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts b/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts index c7d17ebb..9caeec15 100644 --- a/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts +++ b/client-react/src/assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts @@ -10,6 +10,10 @@ const styles = createStyles((theme: Theme) => ({ width: '120px', }, }, + retryButtonContainer: { + textAlign: 'center', + paddingTop: theme.spacing.unit * 2, + }, })); export default styles; \ No newline at end of file diff --git a/client-react/src/components/CircleLoader/CircleLoader.module.scss b/client-react/src/components/CircleLoader/CircleLoader.module.scss new file mode 100644 index 00000000..9763156b --- /dev/null +++ b/client-react/src/components/CircleLoader/CircleLoader.module.scss @@ -0,0 +1,148 @@ +$brand-success: #5cb85c; +$brand-failure: #d44141; +$loader-size: 4em; +$check-height: $loader-size/2; +$check-width: $check-height/2; +$check-left: ($loader-size/6 + $loader-size/25); +$check-thickness: 3px; +$check-color: $brand-success; + +$cross-height: $loader-size/2; +$cross-width: $check-height/10 - $check-height/12; +$cross-left: $loader-size/2; +$cross-top: $loader-size/4; + +.circleLoader { + border: 1px solid rgba(0, 0, 0, 0.2); + border-left-color: $check-color; + animation: loader-spin 1.2s infinite linear; + position: relative; + display: inline-block; + vertical-align: top; + border-radius: 50%; + width: $loader-size; + height: $loader-size; +} + +.loadComplete { + -webkit-animation: none; + animation: none; + transition: border 500ms ease-out; + &.success { + border-color: $brand-success; + } + &.failure { + border-color: $brand-failure; + } +} + +.checkmark { + display: none; + + &.show { + display: inline; + } + + &.draw:after { + animation-duration: 800ms; + animation-timing-function: ease; + animation-name: checkmark; + transform: scaleX(-1) rotate(135deg); + } + + &:after { + opacity: 1; + height: $check-height; + width: $check-width; + transform-origin: left top; + border-right: $check-thickness solid $check-color; + border-top: $check-thickness solid $check-color; + content: ''; + left: $check-left; + top: $check-height; + position: absolute; + } +} + +.cross { + display: none; + + &.show { + display: inline; + } + + &.draw:after, &.draw:before { + animation-duration: 300ms; + animation-timing-function: ease; + animation-name: cross; + } + + &:before, &:after { + position: absolute; + left: $cross-left; + top: $cross-top; + content: ''; + height: $cross-height; + width: $cross-width; + background-color: $brand-failure; + } + + &:before { + transform: rotate(45deg); + } + + &:after { + transform: rotate(-45deg); + } +} + +@keyframes loader-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes checkmark { + 0% { + height: 0; + width: 0; + opacity: 1; + } + 20% { + height: 0; + width: $check-width; + opacity: 1; + } + 40% { + height: $check-height; + width: $check-width; + opacity: 1; + } + 100% { + height: $check-height; + width: $check-width; + opacity: 1; + } +} + +@keyframes cross { + 0% { + width: 0; + height: 0; + } + 20% { + height: 0; + width: $cross-width; + } + 40% { + height: $cross-height; + width: $cross-width; + } + 100% { + width: $cross-width; + height: $cross-height; + } +} \ No newline at end of file diff --git a/client-react/src/components/CircleLoader/CircleLoader.tsx b/client-react/src/components/CircleLoader/CircleLoader.tsx new file mode 100644 index 00000000..c5c44a6d --- /dev/null +++ b/client-react/src/components/CircleLoader/CircleLoader.tsx @@ -0,0 +1,44 @@ +import React, { Component } from "react"; +import classnames from 'classnames'; + +import styles from './CircleLoader.module.scss'; + +export enum Status { + LOADING, + SUCCESSFUL, + FAILURE, +} + +export interface Props { + status: Status; +} + +class CircleLoader extends Component { + render() { + const containerClasses = [styles.circleLoader]; + const checkmarkClasses = [styles.checkmark, styles.draw]; + const crossClasses = [styles.cross, styles.draw]; + + if (this.props.status === Status.SUCCESSFUL) { + containerClasses.push(styles.loadComplete); + containerClasses.push(styles.success); + checkmarkClasses.push(styles.show); + } + else if (this.props.status === Status.FAILURE) { + containerClasses.push(styles.loadComplete); + containerClasses.push(styles.failure); + crossClasses.push(styles.show); + } + + const key = 'container-' + this.props.status; + + return ( +
+ {
} + {
} +
+ ) + } +} + +export default CircleLoader; \ No newline at end of file diff --git a/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts b/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts index 7dbd7756..c525cfa7 100644 --- a/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts +++ b/client-react/src/containers/components/StateSynchronizer/StateSynchronizer.ts @@ -1,14 +1,14 @@ import { connect } from 'react-redux'; import StateSynchronizer, { OnLoaded, OnError } from '../../../components/StateSynchronizer/StateSynchronizer'; import { RootState } from '../../../reducers'; -import { fetchStateSuccess, fetchState, fetchStateFailure } from '../../../reducers/Portal/actions'; +import { fetchStateSuccess, fetchState, fetchStateFailure } from '../../../reducers/Portal/FirstFactor/actions'; import RemoteState from '../../../reducers/Portal/RemoteState'; import { Dispatch } from 'redux'; const mapStateToProps = (state: RootState) => ({ - state: state.remoteState, - stateError: state.remoteStateError, - stateLoading: state.remoteStateLoading, + state: state.firstFactor.remoteState, + stateError: state.firstFactor.remoteStateError, + stateLoading: state.firstFactor.remoteStateLoading, }); const mapDispatchToProps = (dispatch: Dispatch) => { diff --git a/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts b/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts index 602a6c62..b426550d 100644 --- a/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts +++ b/client-react/src/containers/layouts/PortalLayout/PortalLayout.ts @@ -3,7 +3,7 @@ import PortalLayout from '../../../layouts/PortalLayout/PortalLayout'; import { RootState } from '../../../reducers'; const mapStateToProps = (state: RootState) => ({ - authenticationLevel: (state.remoteState) ? state.remoteState.authentication_level : 0, + authenticationLevel: (state.firstFactor.remoteState) ? state.firstFactor.remoteState.authentication_level : 0, }); export default connect(mapStateToProps)(PortalLayout); \ No newline at end of file diff --git a/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts b/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts index d13e6685..1ab101d4 100644 --- a/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts +++ b/client-react/src/containers/views/FirstFactorView/FirstFactorView.ts @@ -1,12 +1,28 @@ import { connect } from 'react-redux'; +import QueryString from 'query-string'; import FirstFactorView, { Props } from '../../../views/FirstFactorView/FirstFactorView'; import { Dispatch } from 'redux'; -import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/actions'; +import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions'; import { RootState } from '../../../reducers'; const mapStateToProps = (state: RootState) => ({}); +function redirect2FA(props: Props) { + if (!props.location) { + props.history.push('/2fa'); + return; + } + const params = QueryString.parse(props.location.search); + + if ('rd' in params) { + const rd = params['rd'] as string; + props.history.push(`/2fa?rd=${rd}`); + return; + } + props.history.push('/2fa'); +} + function onAuthenticationRequested(dispatch: Dispatch, ownProps: Props) { return async (username: string, password: string) => { dispatch(authenticate()); @@ -27,7 +43,7 @@ function onAuthenticationRequested(dispatch: Dispatch, ownProps: Props) { return; } dispatch(authenticateSuccess()); - ownProps.history.push('/2fa'); + redirect2FA(ownProps); }); } } diff --git a/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts b/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts index bfdb5ad3..dbc356a6 100644 --- a/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts +++ b/client-react/src/containers/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.ts @@ -1,10 +1,15 @@ import { connect } from 'react-redux'; -import OneTimePasswordRegistrationView, { OnSuccess, OnFailure } from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView'; +import OneTimePasswordRegistrationView from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView'; import { RootState } from '../../../reducers'; import { Dispatch } from 'redux'; import {to} from 'await-to-js'; +import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions'; +import { Props } from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView'; -const mapStateToProps = (state: RootState) => ({}); +const mapStateToProps = (state: RootState) => ({ + error: state.oneTimePasswordRegistration.error, + secret: state.oneTimePasswordRegistration.secret, +}); async function checkIdentity(token: string) { return fetch(`/api/secondfactor/totp/identity/finish?token=${token}`, { @@ -27,16 +32,35 @@ async function checkIdentity(token: string) { }); } -const mapDispatchToProps = (dispatch: Dispatch) => { +async function tryGenerateTotpSecret(dispatch: Dispatch, token: string) { + let err, result; + dispatch(generateTotpSecret()); + [err, result] = await to(checkIdentity(token)); + if (err) { + const e = err; + setTimeout(() => { + dispatch(generateTotpSecretFailure(e.message)); + }, 2000); + return; + } + dispatch(generateTotpSecretSuccess(result)); +} + +const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { + let internalToken: string; return { - componentDidMount: async (token: string, onSuccess: OnSuccess, onFailure: OnFailure) => { - let err, result; - [err, result] = await to(checkIdentity(token)); - if (err) { - onFailure(err); - return; - } - onSuccess(result.otpauth_url); + onInit: async (token: string) => { + internalToken = token; + await tryGenerateTotpSecret(dispatch, internalToken); + }, + onRetryClicked: async () => { + await tryGenerateTotpSecret(dispatch, internalToken); + }, + onCancelClicked: () => { + ownProps.history.push('/2fa'); + }, + onLoginClicked: () => { + ownProps.history.push('/2fa'); } } } diff --git a/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts b/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts index 404f7eb0..2364c99d 100644 --- a/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts +++ b/client-react/src/containers/views/SecondFactorView/SecondFactorView.ts @@ -1,16 +1,20 @@ import { connect } from 'react-redux'; +import QueryString from 'query-string'; import SecondFactorView, {Props} from '../../../views/SecondFactorView/SecondFactorView'; import { RootState } from '../../../reducers'; import { Dispatch } from 'redux'; import u2fApi, { SignResponse } from 'u2f-api'; import to from 'await-to-js'; -import { logoutSuccess, logoutFailure, logout } from '../../../reducers/Portal/actions'; +import { logoutSuccess, logoutFailure, logout, securityKeySignSuccess, securityKeySign, securityKeySignFailure, setSecurityKeySupported } from '../../../reducers/Portal/SecondFactor/actions'; import AuthenticationLevel from '../../../types/AuthenticationLevel'; import RemoteState from '../../../reducers/Portal/RemoteState'; const mapStateToProps = (state: RootState) => ({ - state: state.remoteState, - stateError: state.remoteStateError, + state: state.firstFactor.remoteState, + stateError: state.firstFactor.remoteStateError, + securityKeySupported: state.secondFactor.securityKeySupported, + securityKeyVerified: state.secondFactor.securityKeySignSuccess || false, + securityKeyError: state.secondFactor.error, }); async function requestSigning() { @@ -23,7 +27,7 @@ async function requestSigning() { }); } -async function completeSigning(response: u2fApi.SignResponse) { +async function completeSecurityKeySigning(response: u2fApi.SignResponse) { return fetch('/api/u2f/sign', { method: 'POST', headers: { @@ -39,25 +43,37 @@ async function completeSigning(response: u2fApi.SignResponse) { }); } -async function triggerSecurityKeySigning() { +async function triggerSecurityKeySigning(dispatch: Dispatch, props: Props) { let err, result; + dispatch(securityKeySign()); [err, result] = await to(requestSigning()); if (err) { - console.error(err); + dispatch(securityKeySignFailure(err.message)); return; } [err, result] = await to(u2fApi.sign(result, 60)); if (err) { - console.error(err); + dispatch(securityKeySignFailure(err.message)); return; } - [err, result] = await to(completeSigning(result as SignResponse)); + [err, result] = await to(completeSecurityKeySigning(result as SignResponse)); if (err) { - console.error(err); + dispatch(securityKeySignFailure(err.message)); return; } + dispatch(securityKeySignSuccess()); + await redirectUponAuthentication(props); +} + +async function redirectUponAuthentication(props: Props) { + const params = QueryString.parse(props.history.location.search); + if ('rd' in params) { + setTimeout(() => { + window.location.replace(params['rd'] as string); + }, 1500); + } } const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { @@ -108,7 +124,11 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { ownProps.history.push('/'); return; } - await triggerSecurityKeySigning(); + const isU2FSupported = await u2fApi.isSupported(); + if (isU2FSupported) { + await dispatch(setSecurityKeySupported(true)); + await triggerSecurityKeySigning(dispatch, ownProps); + } } } } diff --git a/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts b/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts index 6f139cec..f51fcb45 100644 --- a/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts +++ b/client-react/src/containers/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.ts @@ -4,8 +4,13 @@ import { RootState } from '../../../reducers'; import { Dispatch } from 'redux'; import {to} from 'await-to-js'; import * as U2fApi from "u2f-api"; +import { Props } from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView'; +import { registerSecurityKey, registerSecurityKeyFailure, registerSecurityKeySuccess } from '../../../reducers/Portal/SecurityKeyRegistration/actions'; -const mapStateToProps = (state: RootState) => ({}); +const mapStateToProps = (state: RootState) => ({ + deviceRegistered: state.securityKeyRegistration.success, + error: state.securityKeyRegistration.error, +}); async function checkIdentity(token: string) { return fetch(`/api/secondfactor/u2f/identity/finish?token=${token}`, { @@ -39,32 +44,45 @@ async function completeRegistration(response: U2fApi.RegisterResponse) { }); } -const mapDispatchToProps = (dispatch: Dispatch) => { +function fail(dispatch: Dispatch, err: Error) { + dispatch(registerSecurityKeyFailure(err.message)); +} + +const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { return { - componentDidMount: async (token: string) => { + onInit: async (token: string) => { let err, result; + dispatch(registerSecurityKey()); [err, result] = await to(checkIdentity(token)); if (err) { - console.error(err); + fail(dispatch, err); return; } [err, result] = await to(requestRegistration()); if (err) { - console.error(err); + fail(dispatch, err); return; } [err, result] = await to(U2fApi.register(result, [], 60)); if (err) { - console.error(err); + fail(dispatch, err); return; } [err, result] = await to(completeRegistration(result as U2fApi.RegisterResponse)); if (err) { - console.error(err); + fail(dispatch, err); return; } + + dispatch(registerSecurityKeySuccess()); + setTimeout(() => { + ownProps.history.push('/2fa'); + }, 2000); + }, + onBackClicked: () => { + ownProps.history.push('/2fa'); } } } diff --git a/client-react/src/reducers/Portal/actions.ts b/client-react/src/reducers/Portal/FirstFactor/actions.ts similarity index 69% rename from client-react/src/reducers/Portal/actions.ts rename to client-react/src/reducers/Portal/FirstFactor/actions.ts index 5548dcd5..005389ba 100644 --- a/client-react/src/reducers/Portal/actions.ts +++ b/client-react/src/reducers/Portal/FirstFactor/actions.ts @@ -6,11 +6,8 @@ import { FETCH_STATE_REQUEST, FETCH_STATE_SUCCESS, FETCH_STATE_FAILURE, - LOGOUT_REQUEST, - LOGOUT_SUCCESS, - LOGOUT_FAILURE -} from "../constants"; -import RemoteState from './RemoteState'; +} from "../../constants"; +import RemoteState from '../RemoteState'; /* FETCH_STATE */ export const fetchState = createAction(FETCH_STATE_REQUEST); @@ -31,11 +28,3 @@ export const authenticateSuccess = createAction(AUTHENTICATE_SUCCESS); export const authenticateFailure = createAction(AUTHENTICATE_FAILURE, resolve => { return (error: string) => resolve(error); }); - - -/* AUTHENTICATE_REQUEST */ -export const logout = createAction(LOGOUT_REQUEST); -export const logoutSuccess = createAction(LOGOUT_SUCCESS); -export const logoutFailure = createAction(LOGOUT_FAILURE, resolve => { - return (error: string) => resolve(error); -}); diff --git a/client-react/src/reducers/Portal/reducer.ts b/client-react/src/reducers/Portal/FirstFactor/reducer.ts similarity index 72% rename from client-react/src/reducers/Portal/reducer.ts rename to client-react/src/reducers/Portal/FirstFactor/reducer.ts index 203fbcbf..07ac6d60 100644 --- a/client-react/src/reducers/Portal/reducer.ts +++ b/client-react/src/reducers/Portal/FirstFactor/reducer.ts @@ -1,7 +1,7 @@ import * as Actions from './actions'; import { ActionType, getType, StateType } from 'typesafe-actions'; -import RemoteState from './RemoteState'; +import RemoteState from '../RemoteState'; export type FirstFactorAction = ActionType; @@ -19,10 +19,6 @@ interface State { remoteState: RemoteState | null; remoteStateLoading: boolean; remoteStateError: string | null; - - logoutLoading: boolean; - logoutSuccess: boolean | null; - logoutError: string | null; } const initialState: State = { @@ -32,9 +28,6 @@ const initialState: State = { remoteState: null, remoteStateLoading: false, remoteStateError: null, - logoutLoading: false, - logoutError: null, - logoutSuccess: null, } export type PortalState = StateType; @@ -81,26 +74,6 @@ export default (state = initialState, action: FirstFactorAction) => { remoteStateError: action.payload, remoteStateLoading: false, }; - - case getType(Actions.logout): - return { - ...state, - logoutLoading: true, - logoutSuccess: null, - logoutError: null, - }; - case getType(Actions.logoutSuccess): - return { - ...state, - logoutLoading: false, - logoutSuccess: true, - }; - case getType(Actions.logoutFailure): - return { - ...state, - logoutLoading: false, - logoutError: action.payload, - } } return state; } \ No newline at end of file diff --git a/client-react/src/reducers/Portal/OneTimePasswordRegistration/actions.ts b/client-react/src/reducers/Portal/OneTimePasswordRegistration/actions.ts new file mode 100644 index 00000000..f9a90dba --- /dev/null +++ b/client-react/src/reducers/Portal/OneTimePasswordRegistration/actions.ts @@ -0,0 +1,13 @@ +import { createAction } from "typesafe-actions"; +import { GENERATE_TOTP_SECRET_REQUEST, GENERATE_TOTP_SECRET_SUCCESS, GENERATE_TOTP_SECRET_FAILURE } from "../../constants"; +import { Secret } from "../../../views/OneTimePasswordRegistrationView/Secret"; + + +/* GENERATE_TOTP_SECRET_REQUEST */ +export const generateTotpSecret = createAction(GENERATE_TOTP_SECRET_REQUEST); +export const generateTotpSecretSuccess = createAction(GENERATE_TOTP_SECRET_SUCCESS, resolve => { + return (secret: Secret) => resolve(secret); +}); +export const generateTotpSecretFailure = createAction(GENERATE_TOTP_SECRET_FAILURE, resolve => { + return (error: string) => resolve(error); +}); diff --git a/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts b/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts new file mode 100644 index 00000000..8d3d8a71 --- /dev/null +++ b/client-react/src/reducers/Portal/OneTimePasswordRegistration/reducer.ts @@ -0,0 +1,50 @@ +import { ActionType, getType } from "typesafe-actions"; +import * as Actions from './actions'; +import { Secret } from "../../../views/OneTimePasswordRegistrationView/Secret"; + +type OneTimePasswordRegistrationAction = ActionType + +export interface State { + loading: boolean; + error: string | null; + secret: Secret | null; +} + +let initialState: State = { + loading: true, + error: null, + secret: null, +} + +initialState = { + secret: { + base32_secret: 'PBSFWU2RM42HG3TNIRHUQMKSKVUW6NCNOBNFOLCFJZATS6CTI47A', + otpauth_url: 'PBSFWU2RM42HG3TNIRHUQMKSKVUW6NCNOBNFOLCFJZATS6CTI47A', + }, + error: null, + loading: false, +} + +export default (state = initialState, action: OneTimePasswordRegistrationAction) => { + switch(action.type) { + case getType(Actions.generateTotpSecret): + return { + loading: true, + error: null, + secret: null, + }; + case getType(Actions.generateTotpSecretSuccess): + return { + ...state, + loading: false, + secret: action.payload, + } + case getType(Actions.generateTotpSecretFailure): + return { + ...state, + loading: false, + error: action.payload, + } + } + return state; +} \ No newline at end of file diff --git a/client-react/src/reducers/Portal/SecondFactor/actions.ts b/client-react/src/reducers/Portal/SecondFactor/actions.ts new file mode 100644 index 00000000..befe92c5 --- /dev/null +++ b/client-react/src/reducers/Portal/SecondFactor/actions.ts @@ -0,0 +1,26 @@ +import { createAction } from "typesafe-actions"; +import { + LOGOUT_REQUEST, + LOGOUT_SUCCESS, + LOGOUT_FAILURE, + SECURITY_KEY_SIGN, + SECURITY_KEY_SIGN_SUCCESS, + SECURITY_KEY_SIGN_FAILURE, + SET_SECURITY_KEY_SUPPORTED +} from "../../constants"; + +export const setSecurityKeySupported = createAction(SET_SECURITY_KEY_SUPPORTED, resolve => { + return (supported: boolean) => resolve(supported); +}) + +export const securityKeySign = createAction(SECURITY_KEY_SIGN); +export const securityKeySignSuccess = createAction(SECURITY_KEY_SIGN_SUCCESS); +export const securityKeySignFailure = createAction(SECURITY_KEY_SIGN_FAILURE, resolve => { + return (error: string) => resolve(error); +}) + +export const logout = createAction(LOGOUT_REQUEST); +export const logoutSuccess = createAction(LOGOUT_SUCCESS); +export const logoutFailure = createAction(LOGOUT_FAILURE, resolve => { + return (error: string) => resolve(error); +}); diff --git a/client-react/src/reducers/Portal/SecondFactor/reducer.ts b/client-react/src/reducers/Portal/SecondFactor/reducer.ts new file mode 100644 index 00000000..e3d6f937 --- /dev/null +++ b/client-react/src/reducers/Portal/SecondFactor/reducer.ts @@ -0,0 +1,75 @@ + +import * as Actions from './actions'; +import { ActionType, getType, StateType } from 'typesafe-actions'; + +export type SecondFactorAction = ActionType; + +interface State { + logoutLoading: boolean; + logoutSuccess: boolean | null; + error: string | null; + + securityKeySupported: boolean; + securityKeySignLoading: boolean; + securityKeySignSuccess: boolean | null; +} + +const initialState: State = { + logoutLoading: false, + logoutSuccess: null, + error: null, + + securityKeySupported: false, + securityKeySignLoading: false, + securityKeySignSuccess: null, +} + +export type PortalState = StateType; + +export default (state = initialState, action: SecondFactorAction): State => { + switch(action.type) { + case getType(Actions.logout): + return { + ...state, + logoutLoading: true, + logoutSuccess: null, + error: null, + }; + case getType(Actions.logoutSuccess): + return { + ...state, + logoutLoading: false, + logoutSuccess: true, + }; + case getType(Actions.logoutFailure): + return { + ...state, + logoutLoading: false, + error: action.payload, + } + case getType(Actions.securityKeySign): + return { + ...state, + securityKeySignLoading: true, + securityKeySignSuccess: false, + }; + case getType(Actions.securityKeySignSuccess): + return { + ...state, + securityKeySignLoading: false, + securityKeySignSuccess: true, + }; + case getType(Actions.securityKeySignFailure): + return { + ...state, + securityKeySignLoading: false, + securityKeySignSuccess: false, + }; + case getType(Actions.setSecurityKeySupported): + return { + ...state, + securityKeySupported: action.payload, + }; + } + return state; +} \ No newline at end of file diff --git a/client-react/src/reducers/Portal/SecurityKeyRegistration/actions.ts b/client-react/src/reducers/Portal/SecurityKeyRegistration/actions.ts new file mode 100644 index 00000000..8ee3b414 --- /dev/null +++ b/client-react/src/reducers/Portal/SecurityKeyRegistration/actions.ts @@ -0,0 +1,8 @@ +import { createAction } from "typesafe-actions"; +import { REGISTER_SECURITY_KEY_REQUEST, REGISTER_SECURITY_KEY_SUCCESS, REGISTER_SECURITY_KEY_FAILURE } from "../../constants"; + +export const registerSecurityKey = createAction(REGISTER_SECURITY_KEY_REQUEST); +export const registerSecurityKeySuccess = createAction(REGISTER_SECURITY_KEY_SUCCESS); +export const registerSecurityKeyFailure = createAction(REGISTER_SECURITY_KEY_FAILURE, resolve => { + return (error: string) => resolve(error); +}); diff --git a/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts b/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts new file mode 100644 index 00000000..41d80bdf --- /dev/null +++ b/client-react/src/reducers/Portal/SecurityKeyRegistration/reducer.ts @@ -0,0 +1,36 @@ +import { ActionType, getType } from "typesafe-actions"; +import * as Actions from './actions'; + +type SecurityKeyRegistrationAction = ActionType + +export interface State { + error: string | null; + success: boolean | null; +} + +let initialState: State = { + error: null, + success: null, +} + +export default (state = initialState, action: SecurityKeyRegistrationAction): State => { + switch(action.type) { + case getType(Actions.registerSecurityKey): + return { + success: null, + error: null, + }; + case getType(Actions.registerSecurityKeySuccess): + return { + ...state, + success: true, + }; + case getType(Actions.registerSecurityKeyFailure): + return { + ...state, + success: false, + error: action.payload, + }; + } + return state; +} \ No newline at end of file diff --git a/client-react/src/reducers/Portal/index.ts b/client-react/src/reducers/Portal/index.ts new file mode 100644 index 00000000..d8537b20 --- /dev/null +++ b/client-react/src/reducers/Portal/index.ts @@ -0,0 +1,13 @@ +import { combineReducers } from 'redux'; + +import FirstFactorReducer from './FirstFactor/reducer'; +import SecondFactorReducer from './SecondFactor/reducer'; +import OneTimePasswordRegistrationReducer from './OneTimePasswordRegistration/reducer'; +import SecurityKeyRegistrationReducer from './SecurityKeyRegistration/reducer'; + +export default combineReducers({ + firstFactor: FirstFactorReducer, + secondFactor: SecondFactorReducer, + oneTimePasswordRegistration: OneTimePasswordRegistrationReducer, + securityKeyRegistration: SecurityKeyRegistrationReducer, +}); \ No newline at end of file diff --git a/client-react/src/reducers/constants.ts b/client-react/src/reducers/constants.ts index a4af97ad..23f8a931 100644 --- a/client-react/src/reducers/constants.ts +++ b/client-react/src/reducers/constants.ts @@ -7,6 +7,23 @@ export const AUTHENTICATE_REQUEST = '@portal/authenticate_request'; export const AUTHENTICATE_SUCCESS = '@portal/authenticate_success'; export const AUTHENTICATE_FAILURE = '@portal/authenticate_failure'; +// SECOND FACTOR PAGE +export const SET_SECURITY_KEY_SUPPORTED = '@portal/second_factor/set_security_key_supported'; + +export const SECURITY_KEY_SIGN = '@portal/second_factor/security_key_sign'; +export const SECURITY_KEY_SIGN_SUCCESS = '@portal/second_factor/security_key_sign_success'; +export const SECURITY_KEY_SIGN_FAILURE = '@portal/second_factor/security_key_sign_failure'; + export const LOGOUT_REQUEST = '@portal/logout_request'; export const LOGOUT_SUCCESS = '@portal/logout_success'; export const LOGOUT_FAILURE = '@portal/logout_failure'; + +// TOTP REGISTRATION +export const GENERATE_TOTP_SECRET_REQUEST = '@portal/generate_totp_secret_request'; +export const GENERATE_TOTP_SECRET_SUCCESS = '@portal/generate_totp_secret_success'; +export const GENERATE_TOTP_SECRET_FAILURE = '@portal/generate_totp_secret_failure'; + +// U2F REGISTRATION +export const REGISTER_SECURITY_KEY_REQUEST = '@portal/security_key_registration/register_request'; +export const REGISTER_SECURITY_KEY_SUCCESS = '@portal/security_key_registration/register_success'; +export const REGISTER_SECURITY_KEY_FAILURE = '@portal/security_key_registration/register_failed'; \ No newline at end of file diff --git a/client-react/src/reducers/index.ts b/client-react/src/reducers/index.ts index 09dcf83f..41da5ac6 100644 --- a/client-react/src/reducers/index.ts +++ b/client-react/src/reducers/index.ts @@ -1,5 +1,6 @@ -import PortalReducer, { PortalState } from './Portal/reducer'; +import PortalReducer from './Portal'; +import { StateType } from 'typesafe-actions'; -export type RootState = PortalState; +export type RootState = StateType; export default PortalReducer; \ No newline at end of file diff --git a/client-react/src/views/FirstFactorView/FirstFactorView.tsx b/client-react/src/views/FirstFactorView/FirstFactorView.tsx index b232468d..2cccfea3 100644 --- a/client-react/src/views/FirstFactorView/FirstFactorView.tsx +++ b/client-react/src/views/FirstFactorView/FirstFactorView.tsx @@ -7,7 +7,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; import { Link } from "react-router-dom"; -import { RouterProps } from "react-router"; +import { RouterProps, RouteProps } from "react-router"; import { WithStyles, withStyles } from "@material-ui/core"; import firstFactorViewStyles from '../../assets/jss/views/FirstFactorView/FirstFactorView'; @@ -18,7 +18,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox'; import StateSynchronizer from "../../containers/components/StateSynchronizer/StateSynchronizer"; import RemoteState from "../../reducers/Portal/RemoteState"; -export interface Props extends RouterProps, WithStyles { +export interface Props extends RouteProps, RouterProps, WithStyles { onAuthenticationRequested(username: string, password: string): void; } @@ -68,7 +68,7 @@ class FirstFactorView extends Component { } } - private renderWithState(state: RemoteState) { + private renderWithState() { const { classes } = this.props; return (
@@ -137,7 +137,7 @@ class FirstFactorView extends Component {
this.setState({remoteState})}/> - {this.state.remoteState ? this.renderWithState(this.state.remoteState) : null} + {this.state.remoteState ? this.renderWithState() : null}
) } @@ -156,10 +156,6 @@ class FirstFactorView extends Component { errorMessage: 'An error occured. Your username/password are probably wrong.' }); } - - onSuccess = () => { - this.props.history.push('/2fa'); - } } export default withStyles(firstFactorViewStyles)(FirstFactorView); \ No newline at end of file diff --git a/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx b/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx index e139cf5d..b7c00c30 100644 --- a/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx +++ b/client-react/src/views/ForgotPasswordView/ForgotPasswordView.tsx @@ -20,7 +20,7 @@ class ForgotPasswordView extends Component { label="E-mail"> +
+
+
Need Google Authenticator?
+ Google Authenticator on Apple Store + Google Authenticator on Google Store
) } + private renderError() { + const {classes} = this.props; + return ( +
+ +
{this.props.error}
+
+
+ + +
+
+ ); + } + + private renderSecret() { + return this.props.secret + ? this.renderWithSecret(this.props.secret) + : this.renderError(); + } + + private renderLoading() { + const { classes } = this.props; + return ( +
+
One-Time password secret is being generated...
+
+
+ ) + } + render() { - return this.state.secret ? this.renderWithSecret(this.state.secret) : null; + return !this.props.secret && !this.props.error + ? this.renderLoading() + : this.renderSecret(); } } diff --git a/client-react/src/views/OneTimePasswordRegistrationView/Secret.ts b/client-react/src/views/OneTimePasswordRegistrationView/Secret.ts new file mode 100644 index 00000000..61e93943 --- /dev/null +++ b/client-react/src/views/OneTimePasswordRegistrationView/Secret.ts @@ -0,0 +1,5 @@ + +export interface Secret { + otpauth_url: string; + base32_secret: string; +} \ No newline at end of file diff --git a/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx b/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx index b7770a36..b1c08d36 100644 --- a/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx +++ b/client-react/src/views/ResetPasswordView/ResetPasswordView.tsx @@ -16,12 +16,14 @@ class ResetPasswordView extends Component { diff --git a/client-react/src/views/SecondFactorView/SecondFactorView.tsx b/client-react/src/views/SecondFactorView/SecondFactorView.tsx index 6e9e78ec..dcc021da 100644 --- a/client-react/src/views/SecondFactorView/SecondFactorView.tsx +++ b/client-react/src/views/SecondFactorView/SecondFactorView.tsx @@ -3,16 +3,18 @@ import React, { Component } from 'react'; import { WithStyles, withStyles, Button, TextField } from '@material-ui/core'; import styles from '../../assets/jss/views/SecondFactorView/SecondFactorView'; -import securityKeyImage from '../../assets/images/security-key-hand.png'; import StateSynchronizer from '../../containers/components/StateSynchronizer/StateSynchronizer'; import { RouterProps, Redirect } from 'react-router'; import RemoteState from '../../reducers/Portal/RemoteState'; import AuthenticationLevel from '../../types/AuthenticationLevel'; import { WithState } from '../../components/StateSynchronizer/WithState'; - -type Mode = 'u2f' | 'totp'; +import CircleLoader, { Status } from '../../components/CircleLoader/CircleLoader'; export interface Props extends WithStyles, RouterProps, WithState { + securityKeySupported: boolean; + securityKeyVerified: boolean; + securityKeyError: string | null; + onLogoutClicked: () => void; onRegisterSecurityKeyClicked: () => void; onRegisterOneTimePasswordClicked: () => void; @@ -20,7 +22,6 @@ export interface Props extends WithStyles, RouterProps, WithState { }; interface State { - mode: Mode; remoteState: RemoteState | null; } @@ -28,43 +29,53 @@ class SecondFactorView extends Component { constructor(props: Props) { super(props); this.state = { - mode: 'u2f', remoteState: null, } } - private toggleMode = () => { - if (this.state.mode === 'u2f') { - this.setState({mode: 'totp'}); - } else if (this.state.mode === 'totp') { - this.setState({mode: 'u2f'}); - } - } - - private renderU2f() { + private renderU2f(n: number) { const { classes } = this.props; + let u2fStatus = Status.LOADING; + if (this.props.securityKeyVerified) { + u2fStatus = Status.SUCCESSFUL; + } else if (this.props.securityKeyError) { + u2fStatus = Status.FAILURE; + } return ( -
-
- security key -
+
+
Option {n} - Security Key
Insert your security key into a USB port and touch the gold disk.
+
+ +
+
) } - private renderTotp() { + private renderTotp(n: number) { const { classes } = this.props; return ( -
-
Provide a one-time password.
+
+
Option {n} - One-Time Password
+ label="One-Time Password"> +
) } diff --git a/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx b/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx index b5aaa5aa..a333ac65 100644 --- a/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx +++ b/client-react/src/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView.tsx @@ -1,18 +1,24 @@ import React, { Component } from "react"; -import securityKeyImage from '../../assets/images/security-key-hand.png'; -import { WithStyles, withStyles } from "@material-ui/core"; +import { WithStyles, withStyles, Button } from "@material-ui/core"; import styles from '../../assets/jss/views/SecurityKeyRegistrationView/SecurityKeyRegistrationView'; -import { RouteProps } from "react-router"; +import { RouteProps, RouterProps } from "react-router"; import QueryString from 'query-string'; +import FormNotification from "../../components/FormNotification/FormNotification"; +import CircleLoader, { Status } from "../../components/CircleLoader/CircleLoader"; -interface Props extends WithStyles, RouteProps { - componentDidMount: (token: string) => void; +export interface Props extends WithStyles, RouteProps, RouterProps { + deviceRegistered: boolean | null; + error: string | null; + onInit: (token: string) => void; + onBackClicked: () => void; } class SecurityKeyRegistrationView extends Component { componentDidMount() { + if (this.props.deviceRegistered) return; + if (!this.props.location) { console.error('There is no location to retrieve query params from...'); return; @@ -22,20 +28,53 @@ class SecurityKeyRegistrationView extends Component { console.error('Token parameter is expected and not provided'); return; } - this.props.componentDidMount(params['token'] as string); + this.props.onInit(params['token'] as string); } - render() { - const {classes} = this.props; + private renderError() { + const { classes } = this.props; return (
-
Press the gold disk to register your security key
-
- security key + + {this.props.error} + +
+
) } + + private renderRegistering() { + const { classes } = this.props; + let status = Status.LOADING; + if (this.props.deviceRegistered === true) { + status = Status.SUCCESSFUL; + } else if (this.props.error) { + status = Status.FAILURE; + } + return ( +
+
Press the gold disk to register your security key
+
+ +
+
+ ) + } + + render() { + if (this.props.error) { + return this.renderError(); + } + + return this.renderRegistering(); + } } export default withStyles(styles)(SecurityKeyRegistrationView); \ No newline at end of file