Merge pull request #330 from clems4ever/react-ui

Rewrite frontend in React and improve development experience.
This commit is contained in:
Clément Michaud 2019-03-03 12:06:04 +01:00 committed by GitHub
commit d1f6502788
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
636 changed files with 28877 additions and 41705 deletions

2
.gitignore vendored
View File

@ -36,3 +36,5 @@ example/ldap/private.ldif
Configuration.schema.json Configuration.schema.json
users_database.test.yml users_database.test.yml
.suite

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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
View 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
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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
View 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

File diff suppressed because it is too large Load Diff

52
client/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

42
client/public/index.html Normal file
View 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>

View 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
View 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
View 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
View 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;

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View 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

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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

View File

@ -0,0 +1,2 @@
$theme-spacing: 10px;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View 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;

View 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;

View 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;

View 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
View File

@ -0,0 +1,3 @@
export const AUTHELIA_URL = "https://www.authelia.com/"
export const AUTHELIA_GITHUB_URL = "https://github.com/clems4ever/authelia";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
.password-reset-form .header-img {
border-radius: 0%;
}

View File

@ -1,4 +0,0 @@
.password-reset-request .header-img {
border-radius: 0%;
}

View File

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

View File

@ -1,5 +0,0 @@
.u2f-register img {
display: block;
margin: 20px auto;
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

11
client/src/index.css Normal file
View 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;
}

View File

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

View 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
export const FORM_SELECTOR = ".form-signin";

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
export const TOTP_FORM_SELECTOR = ".form-signin.totp";
export const TOTP_TOKEN_SELECTOR = ".form-signin #token";

View File

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

View File

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

View File

@ -1,2 +0,0 @@
export const QRCODE_ID_SELECTOR = "#qrcode";

View File

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

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