Merge pull request #330 from clems4ever/react-ui
Rewrite frontend in React and improve development experience.
2
.gitignore
vendored
|
@ -36,3 +36,5 @@ example/ldap/private.ldif
|
||||||
Configuration.schema.json
|
Configuration.schema.json
|
||||||
|
|
||||||
users_database.test.yml
|
users_database.test.yml
|
||||||
|
|
||||||
|
.suite
|
||||||
|
|
|
@ -8,13 +8,11 @@ images/
|
||||||
example/
|
example/
|
||||||
|
|
||||||
.travis.yml
|
.travis.yml
|
||||||
config.test.yml
|
|
||||||
CONTRIBUTORS.md
|
CONTRIBUTORS.md
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
Gruntfile.js
|
Gruntfile.js
|
||||||
tslint.json
|
tslint.json
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
users_database.yml
|
|
||||||
|
|
||||||
*.tgz
|
*.tgz
|
||||||
|
|
10
.travis.yml
|
@ -1,6 +1,6 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- '8'
|
- '9'
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
- ntp
|
- ntp
|
||||||
|
@ -23,16 +23,20 @@ addons:
|
||||||
- public.example.com
|
- public.example.com
|
||||||
- authelia.example.com
|
- authelia.example.com
|
||||||
- admin.example.com
|
- admin.example.com
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- npm install -g npm@'>=2.13.5'
|
- npm install -g npm@'>=2.13.5'
|
||||||
|
- pushd client && npm install && popd
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- export DISPLAY=:99.0
|
- export DISPLAY=:99.0
|
||||||
- sh -e /etc/init.d/xvfb start
|
- sh -e /etc/init.d/xvfb start
|
||||||
- sleep 3
|
- sleep 3
|
||||||
script:
|
script:
|
||||||
- "./scripts/travis.sh"
|
- "./scripts/authelia-scripts travis"
|
||||||
after_success:
|
after_success:
|
||||||
- "./scripts/docker-publish.sh"
|
- "./scripts/authelia-scripts docker publish"
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: npm
|
provider: npm
|
||||||
email: clement.michaud34@gmail.com
|
email: clement.michaud34@gmail.com
|
||||||
|
|
|
@ -12,7 +12,7 @@ RUN apk --update add --no-cache --virtual \
|
||||||
COPY dist/server /usr/src/server
|
COPY dist/server /usr/src/server
|
||||||
COPY dist/shared /usr/src/shared
|
COPY dist/shared /usr/src/shared
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 9091
|
||||||
|
|
||||||
VOLUME /etc/authelia
|
VOLUME /etc/authelia
|
||||||
VOLUME /var/lib/authelia
|
VOLUME /var/lib/authelia
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
FROM node:8.7.0-alpine
|
|
||||||
|
|
||||||
WORKDIR /usr/src
|
|
||||||
|
|
||||||
COPY package.json /usr/src/package.json
|
|
||||||
|
|
||||||
RUN apk --update add --no-cache --virtual \
|
|
||||||
.build-deps make g++ python && \
|
|
||||||
npm install && \
|
|
||||||
apk del .build-deps
|
|
||||||
|
|
||||||
COPY dist/server /usr/src/server
|
|
||||||
COPY dist/shared /usr/src/shared
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
VOLUME /etc/authelia
|
|
||||||
VOLUME /var/lib/authelia
|
|
||||||
|
|
||||||
CMD ["node", "server/src/index.js", "/etc/authelia/config.yml"]
|
|
246
Gruntfile.js
|
@ -1,246 +0,0 @@
|
||||||
module.exports = function (grunt) {
|
|
||||||
const buildDir = "dist";
|
|
||||||
const schemaDir = "server/src/lib/configuration/Configuration.schema.json"
|
|
||||||
var theme = grunt.option('theme') || 'default';
|
|
||||||
|
|
||||||
grunt.initConfig({
|
|
||||||
env: {
|
|
||||||
"env-test-server-unit": {
|
|
||||||
TS_NODE_PROJECT: "server/tsconfig.json"
|
|
||||||
},
|
|
||||||
"env-test-client-unit": {
|
|
||||||
TS_NODE_PROJECT: "client/tsconfig.json"
|
|
||||||
},
|
|
||||||
"env-test-shared-unit": {
|
|
||||||
TS_NODE_PROJECT: "server/tsconfig.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clean: {
|
|
||||||
dist: ['dist'],
|
|
||||||
backup: ['backup'],
|
|
||||||
},
|
|
||||||
run: {
|
|
||||||
"compile-server": {
|
|
||||||
cmd: "./node_modules/.bin/tsc",
|
|
||||||
args: ['-p', 'server/tsconfig.json']
|
|
||||||
},
|
|
||||||
"generate-config-schema": {
|
|
||||||
cmd: "./node_modules/.bin/typescript-json-schema",
|
|
||||||
args: ["-o", schemaDir, "--strictNullChecks",
|
|
||||||
"--required", "server/tsconfig.json", "Configuration"]
|
|
||||||
},
|
|
||||||
"compile-client": {
|
|
||||||
cmd: "./node_modules/.bin/tsc",
|
|
||||||
args: ['-p', 'client/tsconfig.json']
|
|
||||||
},
|
|
||||||
"lint-server": {
|
|
||||||
cmd: "./node_modules/.bin/tslint",
|
|
||||||
args: ['-c', 'server/tslint.json', '-p', 'server/tsconfig.json']
|
|
||||||
},
|
|
||||||
"lint-client": {
|
|
||||||
cmd: "./node_modules/.bin/tslint",
|
|
||||||
args: ['-c', 'client/tslint.json', '-p', 'client/tsconfig.json']
|
|
||||||
},
|
|
||||||
"test-server-unit": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'server/src/**/*.spec.ts']
|
|
||||||
},
|
|
||||||
"test-shared-unit": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'shared/**/*.spec.ts']
|
|
||||||
},
|
|
||||||
"test-client-unit": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'client/test/**/*.test.ts']
|
|
||||||
},
|
|
||||||
"test-cucumber": {
|
|
||||||
cmd: "./scripts/run-cucumber.sh",
|
|
||||||
args: ["./test/features"]
|
|
||||||
},
|
|
||||||
"test-complete-config": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'test/complete-config/**/*.ts']
|
|
||||||
},
|
|
||||||
"test-minimal-config": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'test/minimal-config/**/*.ts']
|
|
||||||
},
|
|
||||||
"test-inactivity": {
|
|
||||||
cmd: "./node_modules/.bin/mocha",
|
|
||||||
args: ['--colors', '--require', 'ts-node/register', 'test/inactivity/**/*.ts']
|
|
||||||
},
|
|
||||||
"docker-build": {
|
|
||||||
cmd: "docker",
|
|
||||||
args: ['build', '-t', 'clems4ever/authelia', '.']
|
|
||||||
},
|
|
||||||
"minify": {
|
|
||||||
cmd: "./node_modules/.bin/uglifyjs",
|
|
||||||
args: [`${buildDir}/server/src/public_html/js/authelia.js`, '-o', `${buildDir}/server/src/public_html/js/authelia.min.js`]
|
|
||||||
},
|
|
||||||
"apidoc": {
|
|
||||||
cmd: "./node_modules/.bin/apidoc",
|
|
||||||
args: ["-i", "src/server", "-o", "doc"]
|
|
||||||
},
|
|
||||||
"include-minified-script": {
|
|
||||||
cmd: "sed",
|
|
||||||
args: ["-i", "s/authelia.\(js\|css\)/authelia.min.\1/", `${buildDir}/server/src/views/layout/layout.pug`]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
copy: {
|
|
||||||
backup: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
src: ['dist/**'],
|
|
||||||
dest: 'backup'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
expand: true,
|
|
||||||
cwd: 'themes/' + theme + '/server/src/resources',
|
|
||||||
src: '**',
|
|
||||||
dest: `${buildDir}/server/src/resources/`
|
|
||||||
},
|
|
||||||
views: {
|
|
||||||
expand: true,
|
|
||||||
cwd: 'themes/' + theme + '/server/src/views',
|
|
||||||
src: '**',
|
|
||||||
dest: `${buildDir}/server/src/views/`
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
expand: true,
|
|
||||||
cwd: 'themes/' + theme + '/client/src/img',
|
|
||||||
src: '**',
|
|
||||||
dest: `${buildDir}/server/src/public_html/img/`
|
|
||||||
},
|
|
||||||
thirdparties: {
|
|
||||||
expand: true,
|
|
||||||
cwd: 'themes/' + theme + '/client/src/thirdparties',
|
|
||||||
src: '**',
|
|
||||||
dest: `${buildDir}/server/src/public_html/js/`
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
src: schemaDir,
|
|
||||||
dest: `${buildDir}/${schemaDir}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
browserify: {
|
|
||||||
dist: {
|
|
||||||
src: ['dist/client/src/index.js'],
|
|
||||||
dest: `${buildDir}/server/src/public_html/js/authelia.js`,
|
|
||||||
options: {
|
|
||||||
browserifyOptions: {
|
|
||||||
standalone: 'authelia'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
views: {
|
|
||||||
files: ['server/src/views/**/*.pug'],
|
|
||||||
tasks: ['copy:views'],
|
|
||||||
options: {
|
|
||||||
interrupt: false,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
files: ['server/src/resources/*.ejs'],
|
|
||||||
tasks: ['copy:resources'],
|
|
||||||
options: {
|
|
||||||
interrupt: false,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
files: ['client/src/img/**'],
|
|
||||||
tasks: ['copy:images'],
|
|
||||||
options: {
|
|
||||||
interrupt: false,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
files: ['client/src/**/*.css'],
|
|
||||||
tasks: ['concat:css', 'cssmin'],
|
|
||||||
options: {
|
|
||||||
interrupt: true,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
client: {
|
|
||||||
files: ['client/src/**/*.ts'],
|
|
||||||
tasks: ['build-dev'],
|
|
||||||
options: {
|
|
||||||
interrupt: true,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
files: ['server/src/**/*.ts'],
|
|
||||||
tasks: ['build-dev', 'run:docker-restart', 'run:make-dev-views' ],
|
|
||||||
options: {
|
|
||||||
interrupt: true,
|
|
||||||
atBegin: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
concat: {
|
|
||||||
css: {
|
|
||||||
src: ['themes/' + theme + '/client/src/css/*.css'],
|
|
||||||
dest: `${buildDir}/server/src/public_html/css/authelia.css`
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cssmin: {
|
|
||||||
target: {
|
|
||||||
files: {
|
|
||||||
[`${buildDir}/server/src/public_html/css/authelia.min.css`]: [`${buildDir}/server/src/public_html/css/authelia.css`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-browserify');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
|
||||||
grunt.loadNpmTasks('grunt-run');
|
|
||||||
grunt.loadNpmTasks('grunt-env');
|
|
||||||
|
|
||||||
grunt.registerTask('compile-server', ['run:lint-server', 'run:compile-server'])
|
|
||||||
grunt.registerTask('compile-client', ['run:lint-client', 'run:compile-client'])
|
|
||||||
|
|
||||||
grunt.registerTask('test-server', ['env:env-test-server-unit', 'run:test-server-unit'])
|
|
||||||
grunt.registerTask('test-shared', ['env:env-test-shared-unit', 'run:test-shared-unit'])
|
|
||||||
grunt.registerTask('test-client', ['env:env-test-client-unit', 'run:test-client-unit'])
|
|
||||||
grunt.registerTask('test-unit', ['test-server', 'test-client', 'test-shared']);
|
|
||||||
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']);
|
|
||||||
|
|
||||||
grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
|
|
||||||
|
|
||||||
grunt.registerTask('generate-config-schema', ['run:generate-config-schema', 'copy:schema']);
|
|
||||||
|
|
||||||
grunt.registerTask('build-client', ['compile-client', 'browserify']);
|
|
||||||
|
|
||||||
grunt.registerTask('build-server', ['compile-server', 'copy-resources', 'generate-config-schema']);
|
|
||||||
|
|
||||||
grunt.registerTask('build', ['build-client', 'build-server']);
|
|
||||||
grunt.registerTask('build-dist', ['clean:backup', 'copy:backup', 'clean:dist', 'build', 'run:minify', 'cssmin', 'run:include-minified-script']);
|
|
||||||
|
|
||||||
grunt.registerTask('schema', ['run:generate-config-schema'])
|
|
||||||
|
|
||||||
grunt.registerTask('docker-build', ['run:docker-build']);
|
|
||||||
|
|
||||||
grunt.registerTask('check', function() {
|
|
||||||
if ((theme != 'default') && (theme != 'black') && (theme != 'matrix') && (theme != 'squares') && (theme != 'triangles')) {
|
|
||||||
grunt.warn('Valid argmuents are just "grunt" (will use default) or "grunt --theme=|default|black|matrix|squares|triangles"');
|
|
||||||
}
|
|
||||||
if (grunt.option('theme') == 'default' || 'black' || 'matrix' || 'squares' || 'triangles') {
|
|
||||||
grunt.log.ok();
|
|
||||||
grunt.log.writeln('Building "'+ theme +'" theme');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.registerTask('default', ['check', 'build-dist']);
|
|
||||||
};
|
|
2
client/.env.development
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
REACT_APP_CSP_CONTENT="default-src 'unsafe-inline'; script-src * 'unsafe-inline'; img-src * data:; style-src 'unsafe-inline'; connect-src * 'unsafe-inline' extensions:"
|
2
client/.env.production
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
REACT_APP_CSP_CONTENT="default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:"
|
23
client/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
9
client/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
FROM node:10.15.0-jessie
|
||||||
|
|
||||||
|
WORKDIR /usr/app/client
|
||||||
|
|
||||||
|
ADD package.json package.json
|
||||||
|
|
||||||
|
RUN npm i
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
44
client/README.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.<br>
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.<br>
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.<br>
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
15
client/docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
authelia-frontend-dev:
|
||||||
|
build:
|
||||||
|
context: client
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./client/tsconfig.json:/usr/app/client/tsconfig.json
|
||||||
|
- ./client/public:/usr/app/client/public
|
||||||
|
- ./client/src:/usr/app/client/src
|
||||||
|
- ./client/.env.development:/usr/app/client/.env.development
|
||||||
|
networks:
|
||||||
|
authelianet:
|
||||||
|
aliases:
|
||||||
|
- authelia-frontend
|
17959
client/package-lock.json
generated
Normal file
52
client/package.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "authelia-portal",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@material/react-button": "^0.8.0",
|
||||||
|
"@material/react-checkbox": "^0.8.0",
|
||||||
|
"@material/react-text-field": "^0.8.0",
|
||||||
|
"@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",
|
||||||
|
"@types/react-dom": "^16.0.9",
|
||||||
|
"@types/react-redux": "^6.0.12",
|
||||||
|
"@types/react-router-dom": "^4.3.1",
|
||||||
|
"@types/redux-thunk": "^2.1.0",
|
||||||
|
"await-to-js": "^2.1.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"connected-react-router": "^6.2.1",
|
||||||
|
"node-sass": "^4.11.0",
|
||||||
|
"qrcode.react": "^0.9.2",
|
||||||
|
"query-string": "^6.2.0",
|
||||||
|
"react": "^16.6.0",
|
||||||
|
"react-dom": "^16.6.0",
|
||||||
|
"react-redux": "^6.0.0",
|
||||||
|
"react-router-dom": "^4.3.1",
|
||||||
|
"react-scripts": "^2.1.3",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
|
"typesafe-actions": "^3.0.0",
|
||||||
|
"typescript": "^3.1.6",
|
||||||
|
"u2f-api": "^1.0.10",
|
||||||
|
"utility-types": "^3.4.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "SASS_PATH=./node_modules react-scripts start",
|
||||||
|
"build": "INLINE_RUNTIME_CHUNK=false SASS_PATH=./node_modules react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all"
|
||||||
|
]
|
||||||
|
}
|
BIN
client/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
42
client/public/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="robots" content="noindex, nofollow, nosnippet, noarchive">
|
||||||
|
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="%REACT_APP_CSP_CONTENT%">
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is added to the
|
||||||
|
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Authelia - Portal</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
13
client/public/manifest.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"short_name": "Authelia",
|
||||||
|
"name": "Authelia - Portal",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
3
client/src/App.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import "@material/react-button/index.scss";
|
||||||
|
@import "@material/react-checkbox/index.scss";
|
||||||
|
@import "@material/react-text-field/index.scss";
|
9
client/src/App.test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
ReactDOM.render(<App />, div);
|
||||||
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
|
});
|
42
client/src/App.tsx
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import './App.scss';
|
||||||
|
|
||||||
|
import { Route, Switch } from "react-router-dom";
|
||||||
|
import { routes } from './routes/index';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { createStore, applyMiddleware, compose } from 'redux';
|
||||||
|
import reducer from './reducers';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import thunk from 'redux-thunk';
|
||||||
|
import { routerMiddleware, ConnectedRouter } from 'connected-react-router';
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
const store = createStore(
|
||||||
|
reducer(history),
|
||||||
|
compose(
|
||||||
|
applyMiddleware(
|
||||||
|
routerMiddleware(history),
|
||||||
|
thunk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<div className="App">
|
||||||
|
<Switch>
|
||||||
|
{routes.map((r, key) => {
|
||||||
|
return <Route path={r.path} component={r.component} key={key}/>
|
||||||
|
})}
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
45
client/src/assets/images/error.svg
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 286.054 286.054" style="enable-background:new 0 0 286.054 286.054;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path style="fill:#E2574C;" d="M168.352,142.924l25.28-25.28c3.495-3.504,3.495-9.154,0-12.64l-12.64-12.649
|
||||||
|
c-3.495-3.486-9.145-3.495-12.64,0l-25.289,25.289l-25.271-25.271c-3.504-3.504-9.163-3.504-12.658-0.018l-12.64,12.649
|
||||||
|
c-3.495,3.486-3.486,9.154,0.018,12.649l25.271,25.271L92.556,168.15c-3.495,3.495-3.495,9.145,0,12.64l12.64,12.649
|
||||||
|
c3.495,3.486,9.145,3.495,12.64,0l25.226-25.226l25.405,25.414c3.504,3.504,9.163,3.504,12.658,0.009l12.64-12.64
|
||||||
|
c3.495-3.495,3.486-9.154-0.009-12.658L168.352,142.924z M143.027,0.004C64.031,0.004,0,64.036,0,143.022
|
||||||
|
c0,78.996,64.031,143.027,143.027,143.027s143.027-64.031,143.027-143.027C286.054,64.045,222.022,0.004,143.027,0.004z
|
||||||
|
M143.027,259.232c-64.183,0-116.209-52.026-116.209-116.209s52.026-116.21,116.209-116.21s116.209,52.026,116.209,116.209
|
||||||
|
S207.21,259.232,143.027,259.232z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
client/src/assets/images/security-key-hand.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
client/src/assets/images/security-key-large.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
client/src/assets/images/security-key.png
Normal file
After Width: | Height: | Size: 11 KiB |
51
client/src/assets/images/user.svg
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
|
||||||
|
<path d="M55,27.5C55,12.337,42.663,0,27.5,0S0,12.337,0,27.5c0,8.009,3.444,15.228,8.926,20.258l-0.026,0.023l0.892,0.752
|
||||||
|
c0.058,0.049,0.121,0.089,0.179,0.137c0.474,0.393,0.965,0.766,1.465,1.127c0.162,0.117,0.324,0.234,0.489,0.348
|
||||||
|
c0.534,0.368,1.082,0.717,1.642,1.048c0.122,0.072,0.245,0.142,0.368,0.212c0.613,0.349,1.239,0.678,1.88,0.98
|
||||||
|
c0.047,0.022,0.095,0.042,0.142,0.064c2.089,0.971,4.319,1.684,6.651,2.105c0.061,0.011,0.122,0.022,0.184,0.033
|
||||||
|
c0.724,0.125,1.456,0.225,2.197,0.292c0.09,0.008,0.18,0.013,0.271,0.021C25.998,54.961,26.744,55,27.5,55
|
||||||
|
c0.749,0,1.488-0.039,2.222-0.098c0.093-0.008,0.186-0.013,0.279-0.021c0.735-0.067,1.461-0.164,2.178-0.287
|
||||||
|
c0.062-0.011,0.125-0.022,0.187-0.034c2.297-0.412,4.495-1.109,6.557-2.055c0.076-0.035,0.153-0.068,0.229-0.104
|
||||||
|
c0.617-0.29,1.22-0.603,1.811-0.936c0.147-0.083,0.293-0.167,0.439-0.253c0.538-0.317,1.067-0.648,1.581-1
|
||||||
|
c0.185-0.126,0.366-0.259,0.549-0.391c0.439-0.316,0.87-0.642,1.289-0.983c0.093-0.075,0.193-0.14,0.284-0.217l0.915-0.764
|
||||||
|
l-0.027-0.023C51.523,42.802,55,35.55,55,27.5z M2,27.5C2,13.439,13.439,2,27.5,2S53,13.439,53,27.5
|
||||||
|
c0,7.577-3.325,14.389-8.589,19.063c-0.294-0.203-0.59-0.385-0.893-0.537l-8.467-4.233c-0.76-0.38-1.232-1.144-1.232-1.993v-2.957
|
||||||
|
c0.196-0.242,0.403-0.516,0.617-0.817c1.096-1.548,1.975-3.27,2.616-5.123c1.267-0.602,2.085-1.864,2.085-3.289v-3.545
|
||||||
|
c0-0.867-0.318-1.708-0.887-2.369v-4.667c0.052-0.52,0.236-3.448-1.883-5.864C34.524,9.065,31.541,8,27.5,8
|
||||||
|
s-7.024,1.065-8.867,3.168c-2.119,2.416-1.935,5.346-1.883,5.864v4.667c-0.568,0.661-0.887,1.502-0.887,2.369v3.545
|
||||||
|
c0,1.101,0.494,2.128,1.34,2.821c0.81,3.173,2.477,5.575,3.093,6.389v2.894c0,0.816-0.445,1.566-1.162,1.958l-7.907,4.313
|
||||||
|
c-0.252,0.137-0.502,0.297-0.752,0.476C5.276,41.792,2,35.022,2,27.5z"/>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
2
client/src/assets/scss/_variables.scss
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
$theme-spacing: 10px;
|
|
@ -0,0 +1,24 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageContainer {
|
||||||
|
color: green;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusIcon {
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutButtonContainer {
|
||||||
|
margin-top: ($theme-spacing) * 2,
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
padding-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rememberMe {
|
||||||
|
float: left;
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: -11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resetPassword {
|
||||||
|
height: 40px;
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resetPassword a {
|
||||||
|
color: black;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: ($theme-spacing) * 2;
|
||||||
|
border: 1px solid red;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #ff8d8d;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative,
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-bottom: ($theme-spacing);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.hello {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
font-size: 0.6em,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding-top: ($theme-spacing) * 2;
|
||||||
|
padding-bottom: ($theme-spacing) * 2;
|
||||||
|
padding-left: ($theme-spacing) * 2;
|
||||||
|
padding-right: ($theme-spacing) * 2;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodName {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: ($theme-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodU2f {
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
padding: ($theme-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodTotp {
|
||||||
|
padding: ($theme-spacing);
|
||||||
|
padding-top: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: '120px';
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerDeviceContainer {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totpField {
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totpButton {
|
||||||
|
margin-top: ($theme-spacing);
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
@import "@material/theme/mdc-theme";
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
width: 440px;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: ($theme-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
box-shadow:
|
||||||
|
rgba(0,0,0,0.14902) 0px 1px 1px 0px,
|
||||||
|
rgba(0,0,0,0.09804) 0px 1px 2px 0px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 40px;
|
||||||
|
border-top: 6px solid ($mdc-theme-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.innerFrame {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.65em;
|
||||||
|
color: grey;
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
display: inline-block
|
||||||
|
}
|
||||||
|
|
||||||
|
.image img {
|
||||||
|
width: 64px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 24%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 76%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding-top: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer {
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
width: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonConfirmContainer {
|
||||||
|
padding-right: ($theme-spacing) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonConfirm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonCancelContainer {
|
||||||
|
padding-left: ($theme-spacing) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonCancel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.secretContainer {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcodeContainer {
|
||||||
|
text-align: center;
|
||||||
|
padding: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base32Container {
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #dcdcdc;
|
||||||
|
padding: ($theme-spacing);
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otpauthContainer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.needGoogleAuthenticator {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.needGoogleAuthenticatorText {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store {
|
||||||
|
width: 100px;
|
||||||
|
margin-top: ($theme-spacing) * 0.5;
|
||||||
|
margin-left: ($theme-spacing) * 0.5;
|
||||||
|
margin-right: ($theme-spacing) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressContainer {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: ($theme-spacing) * 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: ($theme-spacing);
|
||||||
|
margin-right: ($theme-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButtonContainer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.form {
|
||||||
|
margin-top: ($theme-spacing) * 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
width: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonResetContainer {
|
||||||
|
padding-right: ($theme-spacing) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonCancelContainer {
|
||||||
|
padding-left: ($theme-spacing) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.infoContainer {
|
||||||
|
margin-bottom: ($theme-spacing) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.retryButtonContainer {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: ($theme-spacing) * 2;
|
||||||
|
}
|
19
client/src/behaviors/FetchStateBehavior.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import * as AutheliaService from '../services/AutheliaService';
|
||||||
|
import { fetchStateFailure, fetchStateSuccess } from "../reducers/Portal/Authentication/actions";
|
||||||
|
import to from "await-to-js";
|
||||||
|
|
||||||
|
export default async function(dispatch: Dispatch) {
|
||||||
|
let err, res;
|
||||||
|
[err, res] = await to(AutheliaService.fetchState());
|
||||||
|
if (err) {
|
||||||
|
await dispatch(fetchStateFailure(err.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
await dispatch(fetchStateFailure('No response'));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await dispatch(fetchStateSuccess(res));
|
||||||
|
return res;
|
||||||
|
}
|
18
client/src/behaviors/LogoutBehavior.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { logout, logoutFailure, logoutSuccess } from "../reducers/Portal/SecondFactor/actions";
|
||||||
|
import to from "await-to-js";
|
||||||
|
import * as AutheliaService from '../services/AutheliaService';
|
||||||
|
import fetchState from "./FetchStateBehavior";
|
||||||
|
|
||||||
|
export default async function(dispatch: Dispatch) {
|
||||||
|
await dispatch(logout());
|
||||||
|
let err, res;
|
||||||
|
[err, res] = await to(AutheliaService.postLogout());
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
await dispatch(logoutFailure(err.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await dispatch(logoutSuccess());
|
||||||
|
await fetchState(dispatch);
|
||||||
|
}
|
15
client/src/behaviors/SafelyRedirectBehavior.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import * as AutheliaService from '../services/AutheliaService';
|
||||||
|
|
||||||
|
export default async function(url: string) {
|
||||||
|
try {
|
||||||
|
// Check the url against the backend before redirecting.
|
||||||
|
await AutheliaService.checkRedirection(url);
|
||||||
|
window.location.href = url;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'Cannot redirect since the URL is not in the protected domain.' +
|
||||||
|
'This behavior could be malicious so please the issue to an administrator.');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/components/AlreadyAuthenticated/AlreadyAuthenticated.module.scss';
|
||||||
|
import Button from "@material/react-button";
|
||||||
|
import CircleLoader, { Status } from "../CircleLoader/CircleLoader";
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
username: string;
|
||||||
|
redirectionUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {
|
||||||
|
onLogoutClicked: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = OwnProps & DispatchProps;
|
||||||
|
|
||||||
|
class AlreadyAuthenticated extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={classnames(styles.container, 'already-authenticated-step')}>
|
||||||
|
<div className={styles.successContainer}>
|
||||||
|
<div className={styles.messageContainer}>
|
||||||
|
<span className={styles.username}>{this.props.username}</span>
|
||||||
|
you are authenticated
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusIcon}><CircleLoader status={Status.SUCCESSFUL} /></div>
|
||||||
|
</div>
|
||||||
|
<a href={this.props.redirectionUrl}>{this.props.redirectionUrl}</a>
|
||||||
|
<div className={styles.logoutButtonContainer}>
|
||||||
|
<Button
|
||||||
|
onClick={this.props.onLogoutClicked}
|
||||||
|
color="red">
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlreadyAuthenticated;
|
44
client/src/components/CircleLoader/CircleLoader.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/components/CircleLoader/CircleLoader.module.scss';
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
LOADING,
|
||||||
|
SUCCESSFUL,
|
||||||
|
FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
status: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CircleLoader extends Component<Props> {
|
||||||
|
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 (
|
||||||
|
<div className={classnames(containerClasses)} key={key}>
|
||||||
|
{<div className={classnames(checkmarkClasses)} />}
|
||||||
|
{<div className={classnames(crossClasses)} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CircleLoader;
|
143
client/src/components/FirstFactorForm/FirstFactorForm.tsx
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import React, { Component, KeyboardEvent, FormEvent } from "react";
|
||||||
|
|
||||||
|
import TextField, {Input} from '@material/react-text-field';
|
||||||
|
import Button from '@material/react-button';
|
||||||
|
import Checkbox from '@material/react-checkbox';
|
||||||
|
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import Notification from "../../components/Notification/Notification";
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/components/FirstFactorForm/FirstFactorForm.module.scss';
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
redirectionUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateProps {
|
||||||
|
formDisabled: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {
|
||||||
|
onAuthenticationRequested(username: string, password: string, rememberMe: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = OwnProps & StateProps & DispatchProps;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
rememberMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirstFactorForm extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleRememberMe = () => {
|
||||||
|
this.setState({
|
||||||
|
rememberMe: !(this.state.rememberMe)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onUsernameChanged = (e: FormEvent<HTMLElement>) => {
|
||||||
|
const val = (e.target as HTMLInputElement).value;
|
||||||
|
this.setState({username: val});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordChanged = (e: FormEvent<HTMLElement>) => {
|
||||||
|
const val = (e.target as HTMLInputElement).value;
|
||||||
|
this.setState({password: val});
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoginClicked = () => {
|
||||||
|
this.authenticate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordKeyPressed = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.authenticate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='first-factor-step'>
|
||||||
|
<Notification
|
||||||
|
show={this.props.error != null}
|
||||||
|
className={styles.notification}>
|
||||||
|
{this.props.error || ''}
|
||||||
|
</Notification>
|
||||||
|
<div className={styles.fields}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
label="Username"
|
||||||
|
outlined={true}>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
onChange={this.onUsernameChanged}
|
||||||
|
disabled={this.props.formDisabled}
|
||||||
|
value={this.state.username}/>
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
label="Password"
|
||||||
|
outlined={true}>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
disabled={this.props.formDisabled}
|
||||||
|
onChange={this.onPasswordChanged}
|
||||||
|
onKeyPress={this.onPasswordKeyPressed}
|
||||||
|
value={this.state.password} />
|
||||||
|
</TextField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
onClick={this.onLoginClicked}
|
||||||
|
color='primary'
|
||||||
|
raised={true}
|
||||||
|
id='login-button'
|
||||||
|
disabled={this.props.formDisabled}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<div className={styles.rememberMe}>
|
||||||
|
<Checkbox
|
||||||
|
nativeControlId='remember-checkbox'
|
||||||
|
checked={this.state.rememberMe}
|
||||||
|
onChange={this.toggleRememberMe}
|
||||||
|
/>
|
||||||
|
<label htmlFor='remember-checkbox'>Remember me</label>
|
||||||
|
</div>
|
||||||
|
<div className={styles.resetPassword}>
|
||||||
|
<Link to="/forgot-password">Forgot password?</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private authenticate() {
|
||||||
|
this.props.onAuthenticationRequested(
|
||||||
|
this.state.username,
|
||||||
|
this.state.password,
|
||||||
|
this.state.rememberMe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FirstFactorForm;
|
21
client/src/components/Notification/Notification.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/components/Notification/Notification.module.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Notification extends Component<Props> {
|
||||||
|
render() {
|
||||||
|
return (this.props.show)
|
||||||
|
? (<div className={classnames(styles.container, this.props.className, 'notification')}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
162
client/src/components/SecondFactorForm/SecondFactorForm.tsx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import React, { Component, KeyboardEvent, FormEvent } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import TextField, { Input } from '@material/react-text-field';
|
||||||
|
import Button from '@material/react-button';
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/components/SecondFactorForm/SecondFactorForm.module.scss';
|
||||||
|
import CircleLoader, { Status } from '../../components/CircleLoader/CircleLoader';
|
||||||
|
import Notification from '../Notification/Notification';
|
||||||
|
|
||||||
|
export interface OwnProps {
|
||||||
|
username: string;
|
||||||
|
redirectionUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StateProps {
|
||||||
|
securityKeySupported: boolean;
|
||||||
|
securityKeyVerified: boolean;
|
||||||
|
securityKeyError: string | null;
|
||||||
|
|
||||||
|
oneTimePasswordVerificationInProgress: boolean,
|
||||||
|
oneTimePasswordVerificationError: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {
|
||||||
|
onInit: () => void;
|
||||||
|
onLogoutClicked: () => void;
|
||||||
|
onRegisterSecurityKeyClicked: () => void;
|
||||||
|
onRegisterOneTimePasswordClicked: () => void;
|
||||||
|
|
||||||
|
onOneTimePasswordValidationRequested: (token: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = OwnProps & StateProps & DispatchProps;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
oneTimePassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecondFactorView extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
oneTimePassword: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.onInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderU2f(n: number) {
|
||||||
|
let u2fStatus = Status.LOADING;
|
||||||
|
if (this.props.securityKeyVerified) {
|
||||||
|
u2fStatus = Status.SUCCESSFUL;
|
||||||
|
} else if (this.props.securityKeyError) {
|
||||||
|
u2fStatus = Status.FAILURE;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={styles.methodU2f} key='u2f-method'>
|
||||||
|
<div className={styles.methodName}>Option {n} - Security Key</div>
|
||||||
|
<div>Insert your security key into a USB port and touch the gold disk.</div>
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<CircleLoader status={u2fStatus}></CircleLoader>
|
||||||
|
</div>
|
||||||
|
<div className={styles.registerDeviceContainer}>
|
||||||
|
<a className={classnames(styles.registerDevice, 'register-u2f')} href="#"
|
||||||
|
onClick={this.props.onRegisterSecurityKeyClicked}>
|
||||||
|
Register device
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOneTimePasswordChanged = (e: FormEvent<HTMLElement>) => {
|
||||||
|
this.setState({oneTimePassword: (e.target as HTMLInputElement).value});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTotpKeyPressed = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.onOneTimePasswordValidationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onOneTimePasswordValidationRequested = () => {
|
||||||
|
if (this.props.oneTimePasswordVerificationInProgress) return;
|
||||||
|
this.props.onOneTimePasswordValidationRequested(this.state.oneTimePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTotp(n: number) {
|
||||||
|
return (
|
||||||
|
<div className={classnames(styles.methodTotp, 'second-factor-step')} key='totp-method'>
|
||||||
|
<div className={styles.methodName}>Option {n} - One-Time Password</div>
|
||||||
|
<Notification show={this.props.oneTimePasswordVerificationError !== null}>
|
||||||
|
{this.props.oneTimePasswordVerificationError}
|
||||||
|
</Notification>
|
||||||
|
<TextField
|
||||||
|
className={styles.totpField}
|
||||||
|
label="One-Time Password"
|
||||||
|
outlined={true}>
|
||||||
|
<Input
|
||||||
|
name='totp-token'
|
||||||
|
id='totp-token'
|
||||||
|
onChange={this.onOneTimePasswordChanged as any}
|
||||||
|
onKeyPress={this.onTotpKeyPressed}
|
||||||
|
value={this.state.oneTimePassword} />
|
||||||
|
</TextField>
|
||||||
|
<div className={styles.registerDeviceContainer}>
|
||||||
|
<a className={classnames(styles.registerDevice, 'register-totp')} href="#"
|
||||||
|
onClick={this.props.onRegisterOneTimePasswordClicked}>
|
||||||
|
Register device
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.totpButton}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
raised={true}
|
||||||
|
id='totp-button'
|
||||||
|
onClick={this.onOneTimePasswordValidationRequested}
|
||||||
|
disabled={this.props.oneTimePasswordVerificationInProgress}>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderMode() {
|
||||||
|
const methods = [];
|
||||||
|
let n = 1;
|
||||||
|
if (this.props.securityKeySupported) {
|
||||||
|
methods.push(this.renderU2f(n));
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
methods.push(this.renderTotp(n));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.methodsContainer}>
|
||||||
|
{methods}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.hello}>Hello <b>{this.props.username}</b></div>
|
||||||
|
<div className={styles.logout}>
|
||||||
|
<a onClick={this.props.onLogoutClicked} href="#">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.body}>
|
||||||
|
{this.renderMode()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SecondFactorView;
|
3
client/src/constants.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
export const AUTHELIA_URL = "https://www.authelia.com/"
|
||||||
|
export const AUTHELIA_GITHUB_URL = "https://github.com/clems4ever/authelia";
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import AlreadyAuthenticated, { DispatchProps } from '../../../components/AlreadyAuthenticated/AlreadyAuthenticated';
|
||||||
|
import LogoutBehavior from '../../../behaviors/LogoutBehavior';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
|
||||||
|
return {
|
||||||
|
onLogoutClicked: () => LogoutBehavior(dispatch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AlreadyAuthenticated);
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions';
|
||||||
|
import FirstFactorForm, { StateProps, OwnProps } from '../../../components/FirstFactorForm/FirstFactorForm';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import * as AutheliaService from '../../../services/AutheliaService';
|
||||||
|
import to from 'await-to-js';
|
||||||
|
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||||
|
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState): StateProps => {
|
||||||
|
return {
|
||||||
|
error: state.firstFactor.error,
|
||||||
|
formDisabled: state.firstFactor.loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) {
|
||||||
|
return async (username: string, password: string, rememberMe: boolean) => {
|
||||||
|
let err, res;
|
||||||
|
|
||||||
|
// Validate first factor
|
||||||
|
dispatch(authenticate());
|
||||||
|
[err, res] = await to(AutheliaService.postFirstFactorAuth(
|
||||||
|
username, password, rememberMe, redirectionUrl));
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
await dispatch(authenticateFailure(err.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
await dispatch(authenticateFailure('No response'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
const json = await res.json();
|
||||||
|
if ('error' in json) {
|
||||||
|
await dispatch(authenticateFailure(json['error']));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('redirect' in json) {
|
||||||
|
window.location.href = json['redirect'];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (res.status === 204) {
|
||||||
|
dispatch(authenticateSuccess());
|
||||||
|
|
||||||
|
// fetch state to move to next stage
|
||||||
|
FetchStateBehavior(dispatch);
|
||||||
|
} else {
|
||||||
|
dispatch(authenticateFailure('Unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||||
|
return {
|
||||||
|
onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps.redirectionUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(FirstFactorForm);
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import u2fApi from 'u2f-api';
|
||||||
|
import to from 'await-to-js';
|
||||||
|
import { securityKeySignSuccess, securityKeySign, securityKeySignFailure, setSecurityKeySupported, oneTimePasswordVerification, oneTimePasswordVerificationFailure, oneTimePasswordVerificationSuccess } from '../../../reducers/Portal/SecondFactor/actions';
|
||||||
|
import SecondFactorForm, { OwnProps, StateProps } from '../../../components/SecondFactorForm/SecondFactorForm';
|
||||||
|
import * as AutheliaService from '../../../services/AutheliaService';
|
||||||
|
import { push } from 'connected-react-router';
|
||||||
|
import fetchState from '../../../behaviors/FetchStateBehavior';
|
||||||
|
import LogoutBehavior from '../../../behaviors/LogoutBehavior';
|
||||||
|
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState): StateProps => ({
|
||||||
|
securityKeySupported: state.secondFactor.securityKeySupported,
|
||||||
|
securityKeyVerified: state.secondFactor.securityKeySignSuccess || false,
|
||||||
|
securityKeyError: state.secondFactor.error,
|
||||||
|
|
||||||
|
oneTimePasswordVerificationInProgress: state.secondFactor.oneTimePasswordVerificationLoading,
|
||||||
|
oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function triggerSecurityKeySigning(dispatch: Dispatch) {
|
||||||
|
let err, result;
|
||||||
|
dispatch(securityKeySign());
|
||||||
|
[err, result] = await to(AutheliaService.requestSigning());
|
||||||
|
if (err) {
|
||||||
|
await dispatch(securityKeySignFailure(err.message));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
await dispatch(securityKeySignFailure('No response'));
|
||||||
|
throw 'No response';
|
||||||
|
}
|
||||||
|
|
||||||
|
[err, result] = await to(u2fApi.sign(result, 60));
|
||||||
|
if (err) {
|
||||||
|
await dispatch(securityKeySignFailure(err.message));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
await dispatch(securityKeySignFailure('No response'));
|
||||||
|
throw 'No response';
|
||||||
|
}
|
||||||
|
|
||||||
|
[err, result] = await to(AutheliaService.completeSecurityKeySigning(result));
|
||||||
|
if (err) {
|
||||||
|
await dispatch(securityKeySignFailure(err.message));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await dispatch(securityKeySignSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
|
||||||
|
async function handle() {
|
||||||
|
if (ownProps.redirectionUrl) {
|
||||||
|
try {
|
||||||
|
await SafelyRedirectBehavior(ownProps.redirectionUrl);
|
||||||
|
} catch (e) {
|
||||||
|
await fetchState(dispatch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fetchState(dispatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
setTimeout(handle, duration);
|
||||||
|
} else {
|
||||||
|
await handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||||
|
return {
|
||||||
|
onLogoutClicked: () => LogoutBehavior(dispatch),
|
||||||
|
onRegisterSecurityKeyClicked: async () => {
|
||||||
|
await AutheliaService.startU2FRegistrationIdentityProcess();
|
||||||
|
await dispatch(push('/confirmation-sent'));
|
||||||
|
},
|
||||||
|
onRegisterOneTimePasswordClicked: async () => {
|
||||||
|
await AutheliaService.startTOTPRegistrationIdentityProcess();
|
||||||
|
await dispatch(push('/confirmation-sent'));
|
||||||
|
},
|
||||||
|
onInit: async () => {
|
||||||
|
const isU2FSupported = await u2fApi.isSupported();
|
||||||
|
if (isU2FSupported) {
|
||||||
|
await dispatch(setSecurityKeySupported(true));
|
||||||
|
await triggerSecurityKeySigning(dispatch);
|
||||||
|
await handleSuccess(dispatch, ownProps, 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOneTimePasswordValidationRequested: async (token: string) => {
|
||||||
|
let err, res;
|
||||||
|
dispatch(oneTimePasswordVerification());
|
||||||
|
[err, res] = await to(AutheliaService.verifyTotpToken(token));
|
||||||
|
if (err) {
|
||||||
|
dispatch(oneTimePasswordVerificationFailure(err.message));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
dispatch(oneTimePasswordVerificationFailure('No response'));
|
||||||
|
throw 'No response';
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
if ('error' in body) {
|
||||||
|
dispatch(oneTimePasswordVerificationFailure(body['error']));
|
||||||
|
throw body['error'];
|
||||||
|
}
|
||||||
|
dispatch(oneTimePasswordVerificationSuccess());
|
||||||
|
await handleSuccess(dispatch, ownProps);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SecondFactorForm);
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PortalLayout from '../../../layouts/PortalLayout/PortalLayout';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => ({});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PortalLayout);
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import QueryString from 'query-string';
|
||||||
|
import AuthenticationView, {StateProps, Stage, DispatchProps, OwnProps} from '../../../views/AuthenticationView/AuthenticationView';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AuthenticationLevel from '../../../types/AuthenticationLevel';
|
||||||
|
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||||
|
|
||||||
|
function authenticationLevelToStage(level: AuthenticationLevel): Stage {
|
||||||
|
switch (level) {
|
||||||
|
case AuthenticationLevel.NOT_AUTHENTICATED:
|
||||||
|
return Stage.FIRST_FACTOR;
|
||||||
|
case AuthenticationLevel.ONE_FACTOR:
|
||||||
|
return Stage.SECOND_FACTOR;
|
||||||
|
case AuthenticationLevel.TWO_FACTOR:
|
||||||
|
return Stage.ALREADY_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
|
||||||
|
const stage = (state.authentication.remoteState)
|
||||||
|
? authenticationLevelToStage(state.authentication.remoteState.authentication_level)
|
||||||
|
: Stage.FIRST_FACTOR;
|
||||||
|
|
||||||
|
let url: string | null = null;
|
||||||
|
if (ownProps.location) {
|
||||||
|
const params = QueryString.parse(ownProps.location.search);
|
||||||
|
if ('rd' in params) {
|
||||||
|
url = params['rd'] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirectionUrl: url,
|
||||||
|
remoteState: state.authentication.remoteState,
|
||||||
|
stage: stage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
|
return {
|
||||||
|
onInit: async () => await FetchStateBehavior(dispatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationView);
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { push } from 'connected-react-router';
|
||||||
|
import * as AutheliaService from '../../../services/AutheliaService';
|
||||||
|
import ForgotPasswordView from '../../../views/ForgotPasswordView/ForgotPasswordView';
|
||||||
|
import { forgotPasswordRequest, forgotPasswordSuccess, forgotPasswordFailure } from '../../../reducers/Portal/ForgotPassword/actions';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState) => ({
|
||||||
|
disabled: state.forgotPassword.loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
|
return {
|
||||||
|
onPasswordResetRequested: async (username: string) => {
|
||||||
|
try {
|
||||||
|
dispatch(forgotPasswordRequest());
|
||||||
|
await AutheliaService.initiatePasswordResetIdentityValidation(username);
|
||||||
|
dispatch(forgotPasswordSuccess());
|
||||||
|
await dispatch(push('/confirmation-sent'));
|
||||||
|
} catch (err) {
|
||||||
|
dispatch(forgotPasswordFailure(err.message));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancelClicked: async () => {
|
||||||
|
dispatch(push('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ForgotPasswordView);
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
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 { push } from 'connected-react-router';
|
||||||
|
|
||||||
|
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}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Status code ' + res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
if ('error' in body) {
|
||||||
|
throw new Error(body['error']);
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
let internalToken: string;
|
||||||
|
return {
|
||||||
|
onInit: async (token: string) => {
|
||||||
|
internalToken = token;
|
||||||
|
await tryGenerateTotpSecret(dispatch, internalToken);
|
||||||
|
},
|
||||||
|
onRetryClicked: async () => {
|
||||||
|
await tryGenerateTotpSecret(dispatch, internalToken);
|
||||||
|
},
|
||||||
|
onCancelClicked: () => {
|
||||||
|
dispatch(push('/'));
|
||||||
|
},
|
||||||
|
onLoginClicked: () => {
|
||||||
|
dispatch(push('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(OneTimePasswordRegistrationView);
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { RootState } from '../../../reducers';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { push } from 'connected-react-router';
|
||||||
|
import * as AutheliaService from '../../../services/AutheliaService';
|
||||||
|
import ResetPasswordView, { StateProps } from '../../../views/ResetPasswordView/ResetPasswordView';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: RootState): StateProps => ({
|
||||||
|
disabled: state.resetPassword.loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
|
return {
|
||||||
|
onInit: async (token: string) => {
|
||||||
|
await AutheliaService.completePasswordResetIdentityValidation(token);
|
||||||
|
},
|
||||||
|
onPasswordResetRequested: async (newPassword: string) => {
|
||||||
|
await AutheliaService.resetPassword(newPassword);
|
||||||
|
await dispatch(push('/'));
|
||||||
|
},
|
||||||
|
onCancelClicked: async () => {
|
||||||
|
await dispatch(push('/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ResetPasswordView);
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SecurityKeyRegistrationView from '../../../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView';
|
||||||
|
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) => ({
|
||||||
|
deviceRegistered: state.securityKeyRegistration.success,
|
||||||
|
error: state.securityKeyRegistration.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkIdentity(token: string) {
|
||||||
|
return fetch(`/api/secondfactor/u2f/identity/finish?token=${token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestRegistration() {
|
||||||
|
return fetch('/api/u2f/register_request')
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Status code ' + res.status);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeRegistration(response: U2fApi.RegisterResponse) {
|
||||||
|
return fetch('/api/u2f/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(response),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Status code ' + res.status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(dispatch: Dispatch, err: Error) {
|
||||||
|
dispatch(registerSecurityKeyFailure(err.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
|
||||||
|
return {
|
||||||
|
onInit: async (token: string) => {
|
||||||
|
let err, result;
|
||||||
|
dispatch(registerSecurityKey());
|
||||||
|
[err, result] = await to(checkIdentity(token));
|
||||||
|
if (err) {
|
||||||
|
fail(dispatch, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[err, result] = await to(requestRegistration());
|
||||||
|
if (err) {
|
||||||
|
fail(dispatch, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[err, result] = await to(U2fApi.register(result, [], 60));
|
||||||
|
if (err) {
|
||||||
|
fail(dispatch, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[err, result] = await to(completeRegistration(result as U2fApi.RegisterResponse));
|
||||||
|
if (err) {
|
||||||
|
fail(dispatch, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(registerSecurityKeySuccess());
|
||||||
|
setTimeout(() => {
|
||||||
|
ownProps.history.push('/2fa');
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
onBackClicked: () => {
|
||||||
|
ownProps.history.push('/2fa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SecurityKeyRegistrationView);
|
6
client/src/css/00-bootstrap.min.css
vendored
|
@ -1,67 +0,0 @@
|
||||||
body {
|
|
||||||
background-image: url("/img/background.svg");
|
|
||||||
}
|
|
||||||
.authelia-brand {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
color: #648caf
|
|
||||||
}
|
|
||||||
.poweredby-block {
|
|
||||||
margin: 0px 30px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
.poweredby {
|
|
||||||
font-size: 0.7em;
|
|
||||||
color: #6b6b6b;
|
|
||||||
}
|
|
||||||
/* notifications */
|
|
||||||
.notification {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 15px 0px;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.notification img {
|
|
||||||
width: 24px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
.notification i,
|
|
||||||
.notification span {
|
|
||||||
display:table-cell;
|
|
||||||
vertical-align:middle;
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
border: 1px solid #9cb1ff;
|
|
||||||
background-color: rgb(192, 220, 255);
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
border: 1px solid #65ec7c;
|
|
||||||
background-color: rgb(163, 255, 157);
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
border: 1px solid #ffa3a3;
|
|
||||||
background-color: rgb(255, 175, 175);
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
border: 1px solid #ffd743;
|
|
||||||
background-color: rgb(255, 230, 143);
|
|
||||||
}
|
|
||||||
.bottom-right-links {
|
|
||||||
text-align: right;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background-color: #778dab;
|
|
||||||
color: white;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
.body {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 25px;
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
.form-signin
|
|
||||||
{
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin .form-signin-heading, .form-signin .checkbox
|
|
||||||
{
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin .checkbox
|
|
||||||
{
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin .form-control
|
|
||||||
{
|
|
||||||
position: relative;
|
|
||||||
font-size: 16px;
|
|
||||||
height: auto;
|
|
||||||
padding: 10px;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.form-signin .form-control:focus
|
|
||||||
{
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.form-signin input[type="text"]
|
|
||||||
{
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
.form-signin input[type="password"]
|
|
||||||
{
|
|
||||||
/* margin-bottom: 10px; */
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
.account-wall
|
|
||||||
{
|
|
||||||
border: 1px solid #DDD;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
.account-wall h1
|
|
||||||
{
|
|
||||||
margin-bottom: 15px;
|
|
||||||
margin-top: 15px;
|
|
||||||
font-weight: 800;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.account-wall h3
|
|
||||||
{
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.account-wall p
|
|
||||||
{
|
|
||||||
text-align: center;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
.account-wall .form-inputs
|
|
||||||
{
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.account-wall hr {
|
|
||||||
border-color: #c5c5c5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-img
|
|
||||||
{
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
display: block;
|
|
||||||
-moz-border-radius: 50%;
|
|
||||||
-webkit-border-radius: 50%;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link
|
|
||||||
{
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.totp
|
|
||||||
{
|
|
||||||
background-color: rgb(102, 135, 162);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.u2f
|
|
||||||
{
|
|
||||||
background-color: rgb(83, 149, 204);
|
|
||||||
}
|
|
||||||
|
|
||||||
.u2f-token {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u2f-token img {
|
|
||||||
width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keep-me-logged-in {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keep-me-logged-in input[type=checkbox] {
|
|
||||||
transform: scale(0.8);
|
|
||||||
margin: 0;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keep-me-logged-in label {
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keep-me-logged-in input,
|
|
||||||
.keep-me-logged-in label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0; /* I added this after I posted my reply */
|
|
||||||
vertical-align: middle; /* Fixes any weird issues in Firefox and IE */
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
|
|
||||||
.error-401 .header-img {
|
|
||||||
border-radius: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-403 .header-img {
|
|
||||||
border-radius: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-404 .header-img {
|
|
||||||
border-radius: 0%;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
.password-reset-form .header-img {
|
|
||||||
border-radius: 0%;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
.password-reset-request .header-img {
|
|
||||||
border-radius: 0%;
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
.totp-register #secret {
|
|
||||||
background-color: white;
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid #c7c7c7;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.totp-register #qrcode img {
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
.totp-register .need-google-authenticator {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
.totp-register .store-badges {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
.totp-register .store-badge {
|
|
||||||
width: 110px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
|
|
||||||
.u2f-register img {
|
|
||||||
display: block;
|
|
||||||
margin: 20px auto;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="100">
|
|
||||||
<rect width="56" height="100" fill="#FFFFFF"></rect>
|
|
||||||
<path d="M28 66L0 50L0 16L28 0L56 16L56 50L28 66L28 100" fill="none" stroke="#FCFCFC" stroke-width="2"></path>
|
|
||||||
<path d="M28 0L28 34L0 50L0 84L28 100L56 84L56 50L28 34" fill="none" stroke="#FBFBFB" stroke-width="2"></path>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 863 B |
Before Width: | Height: | Size: 732 B |
Before Width: | Height: | Size: 931 B |
Before Width: | Height: | Size: 580 B |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.9 KiB |
11
client/src/index.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
|
|
||||||
import FirstFactorValidator = require("./lib/firstfactor/FirstFactorValidator");
|
|
||||||
|
|
||||||
import FirstFactor from "./lib/firstfactor/index";
|
|
||||||
import SecondFactor from "./lib/secondfactor/index";
|
|
||||||
import TOTPRegister from "./lib/totp-register/totp-register";
|
|
||||||
import U2fRegister from "./lib/u2f-register/u2f-register";
|
|
||||||
import ResetPasswordRequest from "./lib/reset-password/reset-password-request";
|
|
||||||
import ResetPasswordForm from "./lib/reset-password/reset-password-form";
|
|
||||||
import jslogger = require("js-logger");
|
|
||||||
import jQuery = require("jquery");
|
|
||||||
import Endpoints = require("../../shared/api");
|
|
||||||
|
|
||||||
jslogger.useDefaults();
|
|
||||||
jslogger.setLevel(jslogger.INFO);
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
(<any>window).jQuery = jQuery;
|
|
||||||
require("bootstrap");
|
|
||||||
|
|
||||||
jQuery('[data-toggle="tooltip"]').tooltip();
|
|
||||||
if (window.location.pathname == Endpoints.FIRST_FACTOR_GET)
|
|
||||||
FirstFactor(window, jQuery, FirstFactorValidator, jslogger);
|
|
||||||
else if (window.location.pathname == Endpoints.SECOND_FACTOR_GET)
|
|
||||||
SecondFactor(window, jQuery);
|
|
||||||
else if (window.location.pathname == Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET)
|
|
||||||
TOTPRegister(window, jQuery);
|
|
||||||
else if (window.location.pathname == Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET)
|
|
||||||
U2fRegister(window, jQuery);
|
|
||||||
else if (window.location.pathname == Endpoints.RESET_PASSWORD_IDENTITY_FINISH_GET)
|
|
||||||
ResetPasswordForm(window, jQuery);
|
|
||||||
else if (window.location.pathname == Endpoints.RESET_PASSWORD_REQUEST_GET)
|
|
||||||
ResetPasswordRequest(window, jQuery);
|
|
||||||
})();
|
|
12
client/src/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: http://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
52
client/src/layouts/PortalLayout/PortalLayout.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
import { Route, Switch, Redirect, RouterProps, RouteProps } from "react-router";
|
||||||
|
|
||||||
|
import { routes } from '../../routes/routes';
|
||||||
|
import { AUTHELIA_GITHUB_URL } from "../../constants";
|
||||||
|
|
||||||
|
import styles from '../../assets/scss/layouts/PortalLayout/PortalLayout.module.scss';
|
||||||
|
|
||||||
|
interface Props extends RouterProps, RouteProps {}
|
||||||
|
|
||||||
|
class PortalLayout extends Component<Props> {
|
||||||
|
private renderTitle() {
|
||||||
|
if (!this.props.location) return;
|
||||||
|
|
||||||
|
for (let i in routes) {
|
||||||
|
const route = routes[i];
|
||||||
|
if (route.path && route.path.indexOf(this.props.location.pathname) > -1) {
|
||||||
|
return route.title.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className={styles.main}>
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{this.renderTitle()}
|
||||||
|
</div>
|
||||||
|
<div className={styles.frame}>
|
||||||
|
<div className={styles.innerFrame}>
|
||||||
|
<Switch>
|
||||||
|
{routes.map((r, key) => {
|
||||||
|
return <Route path={r.path} component={r.component} exact={true} key={key} />
|
||||||
|
})}
|
||||||
|
<Redirect to='/' />
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div><a href={AUTHELIA_GITHUB_URL}>Powered by Authelia</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PortalLayout;
|
|
@ -1,14 +0,0 @@
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
|
|
||||||
export default function ($: JQueryStatic, url: string, data: Object, fn: any,
|
|
||||||
dataType: string): BluebirdPromise<any> {
|
|
||||||
return new BluebirdPromise<any>((resolve, reject) => {
|
|
||||||
$.get(url, {}, undefined, dataType)
|
|
||||||
.done((data: any) => {
|
|
||||||
resolve(data);
|
|
||||||
})
|
|
||||||
.fail((xhr: JQueryXHR, textStatus: string) => {
|
|
||||||
reject(textStatus);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
|
|
||||||
declare type Handler = () => void;
|
|
||||||
|
|
||||||
export interface Handlers {
|
|
||||||
onFadedIn: Handler;
|
|
||||||
onFadedOut: Handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INotifier {
|
|
||||||
success(msg: string, handlers?: Handlers): void;
|
|
||||||
error(msg: string, handlers?: Handlers): void;
|
|
||||||
warning(msg: string, handlers?: Handlers): void;
|
|
||||||
info(msg: string, handlers?: Handlers): void;
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
import util = require("util");
|
|
||||||
import { INotifier, Handlers } from "./INotifier";
|
|
||||||
|
|
||||||
class NotificationEvent {
|
|
||||||
private element: JQuery;
|
|
||||||
private message: string;
|
|
||||||
private statusType: string;
|
|
||||||
private timeoutId: any;
|
|
||||||
|
|
||||||
constructor(element: JQuery, msg: string, statusType: string) {
|
|
||||||
this.message = msg;
|
|
||||||
this.statusType = statusType;
|
|
||||||
this.element = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearNotification() {
|
|
||||||
this.element.removeClass(this.statusType);
|
|
||||||
this.element.html("");
|
|
||||||
}
|
|
||||||
|
|
||||||
start(handlers?: Handlers) {
|
|
||||||
const that = this;
|
|
||||||
const FADE_TIME = 500;
|
|
||||||
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
|
|
||||||
<span>%s</span>', this.statusType, this.statusType, this.message);
|
|
||||||
this.element.html(html);
|
|
||||||
this.element.addClass(this.statusType);
|
|
||||||
this.element.fadeIn(FADE_TIME, function () {
|
|
||||||
if (handlers)
|
|
||||||
handlers.onFadedIn();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.timeoutId = setTimeout(function () {
|
|
||||||
that.element.fadeOut(FADE_TIME, function () {
|
|
||||||
that.clearNotification();
|
|
||||||
if (handlers)
|
|
||||||
handlers.onFadedOut();
|
|
||||||
});
|
|
||||||
}, 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
interrupt() {
|
|
||||||
this.clearNotification();
|
|
||||||
this.element.hide();
|
|
||||||
clearTimeout(this.timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Notifier implements INotifier {
|
|
||||||
private element: JQuery;
|
|
||||||
private onGoingEvent: NotificationEvent;
|
|
||||||
|
|
||||||
constructor(selector: string, $: JQueryStatic) {
|
|
||||||
this.element = $(selector);
|
|
||||||
this.onGoingEvent = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
|
|
||||||
if (this.onGoingEvent)
|
|
||||||
this.onGoingEvent.interrupt();
|
|
||||||
|
|
||||||
this.onGoingEvent = new NotificationEvent(this.element, msg, statusType);
|
|
||||||
this.onGoingEvent.start(handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
success(msg: string, handlers?: Handlers) {
|
|
||||||
this.displayAndFadeout(msg, "success", handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
error(msg: string, handlers?: Handlers) {
|
|
||||||
this.displayAndFadeout(msg, "error", handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
warning(msg: string, handlers?: Handlers) {
|
|
||||||
this.displayAndFadeout(msg, "warning", handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
info(msg: string, handlers?: Handlers) {
|
|
||||||
this.displayAndFadeout(msg, "info", handlers);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
|
|
||||||
export class QueryParametersRetriever {
|
|
||||||
static get(name: string, url?: string): string {
|
|
||||||
if (!url) url = window.location.href;
|
|
||||||
name = name.replace(/[\[\]]/g, "\\$&");
|
|
||||||
const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
|
||||||
results = regex.exec(url);
|
|
||||||
if (!results) return undefined;
|
|
||||||
if (!results[2]) return "";
|
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { BelongToDomain } from "../../../shared/BelongToDomain";
|
|
||||||
|
|
||||||
export function SafeRedirect(url: string, cb: () => void): void {
|
|
||||||
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
|
||||||
if (url.startsWith("/") || BelongToDomain(url, domain)) {
|
|
||||||
window.location.href = url;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cb();
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import Constants = require("../../../../shared/constants");
|
|
||||||
import Util = require("util");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
|
|
||||||
export function validate(username: string, password: string,
|
|
||||||
keepMeLoggedIn: boolean, redirectUrl: string, $: JQueryStatic)
|
|
||||||
: BluebirdPromise<string> {
|
|
||||||
return new BluebirdPromise<string>(function (resolve, reject) {
|
|
||||||
let url: string;
|
|
||||||
if (redirectUrl != undefined) {
|
|
||||||
const redirectParam = Util.format("%s=%s", Constants.REDIRECT_QUERY_PARAM, redirectUrl);
|
|
||||||
url = Util.format("%s?%s", Endpoints.FIRST_FACTOR_POST, redirectParam);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
url = Util.format("%s", Endpoints.FIRST_FACTOR_POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: any = {
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (keepMeLoggedIn) {
|
|
||||||
data.keepMeLoggedIn = "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
method: "POST",
|
|
||||||
url: url,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
.done(function (body: any) {
|
|
||||||
if (body && body.error) {
|
|
||||||
reject(new Error(body.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(body.redirect);
|
|
||||||
})
|
|
||||||
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
|
||||||
reject(new Error(UserMessages.AUTHENTICATION_FAILED));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
|
|
||||||
export const USERNAME_FIELD_ID = "#username";
|
|
||||||
export const PASSWORD_FIELD_ID = "#password";
|
|
||||||
export const SIGN_IN_BUTTON_ID = "#signin";
|
|
||||||
export const KEEP_ME_LOGGED_IN_ID = "#keep_me_logged_in";
|
|
|
@ -1,49 +0,0 @@
|
||||||
import FirstFactorValidator = require("./FirstFactorValidator");
|
|
||||||
import JSLogger = require("js-logger");
|
|
||||||
import UISelectors = require("./UISelectors");
|
|
||||||
import { Notifier } from "../Notifier";
|
|
||||||
import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
|
||||||
import Constants = require("../../../../shared/constants");
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
import { SafeRedirect } from "../SafeRedirect";
|
|
||||||
|
|
||||||
export default function (window: Window, $: JQueryStatic,
|
|
||||||
firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
|
|
||||||
|
|
||||||
const notifier = new Notifier(".notification", $);
|
|
||||||
|
|
||||||
function onFormSubmitted() {
|
|
||||||
const username: string = $(UISelectors.USERNAME_FIELD_ID).val() as string;
|
|
||||||
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val() as string;
|
|
||||||
const keepMeLoggedIn: boolean = $(UISelectors.KEEP_ME_LOGGED_IN_ID).is(":checked");
|
|
||||||
|
|
||||||
$("form").css("opacity", 0.5);
|
|
||||||
$("input,button").attr("disabled", "true");
|
|
||||||
$(UISelectors.SIGN_IN_BUTTON_ID).text("Please wait...");
|
|
||||||
|
|
||||||
const redirectUrl = QueryParametersRetriever.get(Constants.REDIRECT_QUERY_PARAM);
|
|
||||||
firstFactorValidator.validate(username, password, keepMeLoggedIn, redirectUrl, $)
|
|
||||||
.then(onFirstFactorSuccess, onFirstFactorFailure);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFirstFactorSuccess(redirectUrl: string) {
|
|
||||||
SafeRedirect(redirectUrl, () => {
|
|
||||||
notifier.error("Cannot redirect to an external domain.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFirstFactorFailure(err: Error) {
|
|
||||||
$("input,button").removeAttr("disabled");
|
|
||||||
$("form").css("opacity", 1);
|
|
||||||
notifier.error(UserMessages.AUTHENTICATION_FAILED);
|
|
||||||
$(UISelectors.PASSWORD_FIELD_ID).select();
|
|
||||||
$(UISelectors.SIGN_IN_BUTTON_ID).text("Sign in");
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window.document).ready(function () {
|
|
||||||
$("form").on("submit", onFormSubmitted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
export const FORM_SELECTOR = ".form-signin";
|
|
|
@ -1,57 +0,0 @@
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
|
|
||||||
import Constants = require("./constants");
|
|
||||||
import { Notifier } from "../Notifier";
|
|
||||||
|
|
||||||
export default function (window: Window, $: JQueryStatic) {
|
|
||||||
const notifier = new Notifier(".notification", $);
|
|
||||||
|
|
||||||
function modifyPassword(newPassword: string) {
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
|
||||||
$.post(Endpoints.RESET_PASSWORD_FORM_POST, {
|
|
||||||
password: newPassword,
|
|
||||||
})
|
|
||||||
.done(function (body: any) {
|
|
||||||
if (body && body.error) {
|
|
||||||
reject(new Error(body.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(body);
|
|
||||||
})
|
|
||||||
.fail(function (xhr, status) {
|
|
||||||
reject(status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFormSubmitted() {
|
|
||||||
const password1 = $("#password1").val() as string;
|
|
||||||
const password2 = $("#password2").val() as string;
|
|
||||||
|
|
||||||
if (!password1 || !password2) {
|
|
||||||
notifier.warning(UserMessages.MISSING_PASSWORD);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password1 != password2) {
|
|
||||||
notifier.warning(UserMessages.DIFFERENT_PASSWORDS);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyPassword(password1)
|
|
||||||
.then(function () {
|
|
||||||
window.location.href = Endpoints.FIRST_FACTOR_GET;
|
|
||||||
})
|
|
||||||
.error(function () {
|
|
||||||
notifier.error(UserMessages.RESET_PASSWORD_FAILED);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
import Constants = require("./constants");
|
|
||||||
import jslogger = require("js-logger");
|
|
||||||
import { Notifier } from "../Notifier";
|
|
||||||
|
|
||||||
export default function (window: Window, $: JQueryStatic) {
|
|
||||||
const notifier = new Notifier(".notification", $);
|
|
||||||
|
|
||||||
function requestPasswordReset(username: string) {
|
|
||||||
return new BluebirdPromise(function (resolve, reject) {
|
|
||||||
$.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, {
|
|
||||||
userid: username,
|
|
||||||
})
|
|
||||||
.done(function (body: any) {
|
|
||||||
if (body && body.error) {
|
|
||||||
reject(new Error(body.error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
|
||||||
reject(new Error(textStatus));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFormSubmitted() {
|
|
||||||
const username = $("#username").val() as string;
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
notifier.warning(UserMessages.MISSING_USERNAME);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPasswordReset(username)
|
|
||||||
.then(function () {
|
|
||||||
notifier.success(UserMessages.MAIL_SENT);
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.replace(Endpoints.FIRST_FACTOR_GET);
|
|
||||||
}, 1000);
|
|
||||||
})
|
|
||||||
.error(function () {
|
|
||||||
notifier.error(UserMessages.MAIL_NOT_SENT);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
|
||||||
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
|
||||||
|
|
||||||
export function validate(token: string, $: JQueryStatic): BluebirdPromise<string> {
|
|
||||||
return new BluebirdPromise<string>(function (resolve, reject) {
|
|
||||||
$.ajax({
|
|
||||||
url: Endpoints.SECOND_FACTOR_TOTP_POST,
|
|
||||||
data: {
|
|
||||||
token: token,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
dataType: "json"
|
|
||||||
} as JQueryAjaxSettings)
|
|
||||||
.done(function (body: RedirectionMessage | ErrorMessage) {
|
|
||||||
if (body && "error" in body) {
|
|
||||||
reject(new Error((body as ErrorMessage).error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve((body as RedirectionMessage).redirect);
|
|
||||||
})
|
|
||||||
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
|
||||||
reject(new Error(textStatus));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import U2f = require("u2f");
|
|
||||||
import U2fApi from "u2f-api";
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
import { INotifier } from "../INotifier";
|
|
||||||
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
|
||||||
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
|
||||||
import GetPromised from "../GetPromised";
|
|
||||||
|
|
||||||
function finishU2fAuthentication(responseData: U2fApi.SignResponse,
|
|
||||||
$: JQueryStatic): BluebirdPromise<string> {
|
|
||||||
return new BluebirdPromise<string>(function (resolve, reject) {
|
|
||||||
$.ajax({
|
|
||||||
url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST,
|
|
||||||
data: responseData,
|
|
||||||
method: "POST",
|
|
||||||
dataType: "json"
|
|
||||||
} as JQueryAjaxSettings)
|
|
||||||
.done(function (body: RedirectionMessage | ErrorMessage) {
|
|
||||||
if (body && "error" in body) {
|
|
||||||
reject(new Error((body as ErrorMessage).error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve((body as RedirectionMessage).redirect);
|
|
||||||
})
|
|
||||||
.fail(function (xhr: JQueryXHR, textStatus: string) {
|
|
||||||
reject(new Error(textStatus));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validate($: JQueryStatic): BluebirdPromise<string> {
|
|
||||||
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
|
|
||||||
undefined, "json")
|
|
||||||
.then(function (signRequest: U2f.Request) {
|
|
||||||
return U2fApi.sign(signRequest, 60);
|
|
||||||
})
|
|
||||||
.then(function (signResponse: U2fApi.SignResponse) {
|
|
||||||
return finishU2fAuthentication(signResponse, $);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
|
|
||||||
export const TOTP_FORM_SELECTOR = ".form-signin.totp";
|
|
||||||
export const TOTP_TOKEN_SELECTOR = ".form-signin #token";
|
|
|
@ -1,59 +0,0 @@
|
||||||
import TOTPValidator = require("./TOTPValidator");
|
|
||||||
import U2FValidator = require("./U2FValidator");
|
|
||||||
import ClientConstants = require("./constants");
|
|
||||||
import { Notifier } from "../Notifier";
|
|
||||||
import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
import SharedConstants = require("../../../../shared/constants");
|
|
||||||
import { SafeRedirect } from "../SafeRedirect";
|
|
||||||
|
|
||||||
export default function (window: Window, $: JQueryStatic) {
|
|
||||||
const notifier = new Notifier(".notification", $);
|
|
||||||
|
|
||||||
function onAuthenticationSuccess(serverRedirectUrl: string) {
|
|
||||||
const queryRedirectUrl = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM);
|
|
||||||
if (queryRedirectUrl) {
|
|
||||||
SafeRedirect(queryRedirectUrl, () => {
|
|
||||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
|
||||||
});
|
|
||||||
} else if (serverRedirectUrl) {
|
|
||||||
SafeRedirect(serverRedirectUrl, () => {
|
|
||||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSecondFactorTotpSuccess(redirectUrl: string) {
|
|
||||||
onAuthenticationSuccess(redirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSecondFactorTotpFailure(err: Error) {
|
|
||||||
notifier.error(UserMessages.AUTHENTICATION_TOTP_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onU2fAuthenticationSuccess(redirectUrl: string) {
|
|
||||||
onAuthenticationSuccess(redirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onU2fAuthenticationFailure() {
|
|
||||||
// TODO(clems4ever): we should not display this error message until a device
|
|
||||||
// is registered.
|
|
||||||
// notifier.error(UserMessages.AUTHENTICATION_U2F_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTOTPFormSubmitted(): boolean {
|
|
||||||
const token = $(ClientConstants.TOTP_TOKEN_SELECTOR).val() as string;
|
|
||||||
TOTPValidator.validate(token, $)
|
|
||||||
.then(onSecondFactorTotpSuccess)
|
|
||||||
.catch(onSecondFactorTotpFailure);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window.document).ready(function () {
|
|
||||||
$(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
|
|
||||||
U2FValidator.validate($)
|
|
||||||
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
import jslogger = require("js-logger");
|
|
||||||
import UISelector = require("./ui-selector");
|
|
||||||
|
|
||||||
export default function(window: Window, $: JQueryStatic) {
|
|
||||||
jslogger.debug("Creating QRCode from OTPAuth url");
|
|
||||||
const qrcode = $(UISelector.QRCODE_ID_SELECTOR);
|
|
||||||
const val = qrcode.text();
|
|
||||||
qrcode.empty();
|
|
||||||
new (window as any).QRCode(qrcode.get(0), val);
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
|
|
||||||
export const QRCODE_ID_SELECTOR = "#qrcode";
|
|
|
@ -1,56 +0,0 @@
|
||||||
|
|
||||||
import BluebirdPromise = require("bluebird");
|
|
||||||
import U2f = require("u2f");
|
|
||||||
import * as U2fApi from "u2f-api";
|
|
||||||
import { Notifier } from "../Notifier";
|
|
||||||
import GetPromised from "../GetPromised";
|
|
||||||
import Endpoints = require("../../../../shared/api");
|
|
||||||
import UserMessages = require("../../../../shared/UserMessages");
|
|
||||||
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
|
||||||
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
|
||||||
import { SafeRedirect } from "../SafeRedirect";
|
|
||||||
|
|
||||||
export default function (window: Window, $: JQueryStatic) {
|
|
||||||
const notifier = new Notifier(".notification", $);
|
|
||||||
|
|
||||||
function checkRegistration(regResponse: U2fApi.RegisterResponse): BluebirdPromise<string> {
|
|
||||||
return new BluebirdPromise<string>(function (resolve, reject) {
|
|
||||||
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, regResponse, undefined, "json")
|
|
||||||
.done((body: RedirectionMessage | ErrorMessage) => {
|
|
||||||
if (body && "error" in body) {
|
|
||||||
reject(new Error((body as ErrorMessage).error));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve((body as RedirectionMessage).redirect);
|
|
||||||
})
|
|
||||||
.fail((xhr, status) => {
|
|
||||||
reject(new Error("Failed to register device."));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestRegistration(): BluebirdPromise<string> {
|
|
||||||
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {},
|
|
||||||
undefined, "json")
|
|
||||||
.then((registrationRequest: U2f.Request) => {
|
|
||||||
return U2fApi.register(registrationRequest, [], 60);
|
|
||||||
})
|
|
||||||
.then((res) => checkRegistration(res));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRegisterFailure(err: Error) {
|
|
||||||
notifier.error(UserMessages.REGISTRATION_U2F_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
requestRegistration()
|
|
||||||
.then((redirectionUrl: string) => {
|
|
||||||
SafeRedirect(redirectionUrl, () => {
|
|
||||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
onRegisterFailure(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|