├── .eslintignore
├── .yo-rc.json
├── client
├── public
│ ├── robots.txt
│ ├── favicon.ico
│ ├── img
│ │ ├── icons
│ │ │ ├── favicon.ico
│ │ │ ├── favicon-128.png
│ │ │ ├── mstile-70x70.png
│ │ │ ├── favicon-16x16.ico
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.ico
│ │ │ ├── favicon-32x32.png
│ │ │ ├── favicon-96x96.png
│ │ │ ├── mstile-144x144.png
│ │ │ ├── mstile-150x150.png
│ │ │ ├── mstile-310x150.png
│ │ │ ├── mstile-310x310.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── favicon-196x196.png
│ │ │ ├── android-chrome-144x144.png
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-256x256.png
│ │ │ ├── android-chrome-36x36.png
│ │ │ ├── android-chrome-384x384.png
│ │ │ ├── android-chrome-48x48.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── android-chrome-72x72.png
│ │ │ ├── android-chrome-96x96.png
│ │ │ ├── apple-touch-icon-57x57.png
│ │ │ ├── apple-touch-icon-60x60.png
│ │ │ ├── apple-touch-icon-72x72.png
│ │ │ ├── apple-touch-icon-76x76.png
│ │ │ ├── android-chrome-1024x1024.png
│ │ │ ├── apple-touch-icon-114x114.png
│ │ │ ├── apple-touch-icon-120x120.png
│ │ │ ├── apple-touch-icon-144x144.png
│ │ │ ├── apple-touch-icon-152x152.png
│ │ │ ├── apple-touch-icon-180x180.png
│ │ │ ├── msapplication-icon-144x144.png
│ │ │ └── safari-pinned-tab.svg
│ │ └── adamant-logo-transparent-512x512.png
│ ├── fonts
│ │ ├── Exo+2_100_normal.ttf
│ │ ├── Exo+2_300_normal.ttf
│ │ ├── Exo+2_400_italic.ttf
│ │ ├── Exo+2_400_normal.ttf
│ │ ├── Exo+2_500_normal.ttf
│ │ ├── Exo+2_700_normal.ttf
│ │ ├── Exo+2_100_normal.woff
│ │ ├── Exo+2_300_normal.woff
│ │ ├── Exo+2_400_italic.woff
│ │ ├── Exo+2_400_normal.woff
│ │ ├── Exo+2_500_normal.woff
│ │ ├── Exo+2_700_normal.woff
│ │ ├── Roboto_300_normal.ttf
│ │ ├── Roboto_300_normal.woff
│ │ ├── Roboto_400_italic.ttf
│ │ ├── Roboto_400_italic.woff
│ │ ├── Roboto_400_normal.ttf
│ │ ├── Roboto_400_normal.woff
│ │ ├── Roboto_500_normal.ttf
│ │ ├── Roboto_500_normal.woff
│ │ ├── Roboto_700_normal.ttf
│ │ ├── Roboto_700_normal.woff
│ │ ├── Roboto.css
│ │ ├── Exo+2.css
│ │ └── Roboto_400_normal.svg
│ ├── manifest.json
│ └── index.html
├── .browserslistrc
├── .env
├── babel.config.js
├── README.md
├── postcss.config.js
├── tests
│ ├── unit
│ │ ├── .eslintrc.js
│ │ └── example.spec.js
│ └── e2e
│ │ ├── specs
│ │ └── test.js
│ │ └── custom-assertions
│ │ └── elementCount.js
├── .editorconfig
├── src
│ ├── assets
│ │ └── app.styl
│ ├── main.js
│ ├── plugins
│ │ ├── vuetify.js
│ │ └── i18n.js
│ ├── components
│ │ ├── ScreenLocker.vue
│ │ ├── SnackbarNote.vue
│ │ ├── LanguageSwitcher.vue
│ │ └── NavigationMenu.vue
│ ├── registerServiceWorker.js
│ ├── App.vue
│ ├── router.js
│ ├── locales
│ │ ├── en.json
│ │ └── ru.json
│ ├── views
│ │ ├── Verify.vue
│ │ ├── Signup.vue
│ │ ├── Login.vue
│ │ └── Settings.vue
│ └── store.js
├── .gitignore
├── vue.config.js
├── .eslintrc.js
├── jest.config.js
├── server.js
└── package.json
├── server
├── component-config.json
├── middleware.development.json
├── middleware
│ └── error-handler.js
├── boot
│ ├── root.js
│ ├── authentication.js
│ └── role-resolver.js
├── datasources.json
├── config.json
├── create-lb-tables.js
├── server.js
├── model-config.json
└── middleware.json
├── .gitignore
├── .editorconfig
├── common
└── models
│ ├── test
│ ├── config.json
│ ├── middleware.json
│ └── server.test.js
│ ├── account.json
│ └── account.js
├── .eslintrc.js
├── helpers
├── test
│ ├── constants.js
│ └── logger.test.js
└── logger.js
├── config.default.json
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | /client/
--------------------------------------------------------------------------------
/.yo-rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "generator-loopback": {}
3 | }
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/client/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 |
--------------------------------------------------------------------------------
/client/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_I18N_LOCALE=ru
2 | VUE_APP_I18N_FALLBACK_LOCALE=en
3 |
--------------------------------------------------------------------------------
/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Client part of ADAMANT 2FA Demo Application
2 |
3 | See repository's README.md.
4 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/img/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon.ico
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_100_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_100_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_300_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_300_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_400_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_400_italic.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_400_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_400_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_500_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_500_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_700_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_700_normal.ttf
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-128.png
--------------------------------------------------------------------------------
/client/public/img/icons/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/mstile-70x70.png
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_100_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_100_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_300_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_300_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_400_italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_400_italic.woff
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_400_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_400_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_500_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_500_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2_700_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Exo+2_700_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_300_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_300_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_300_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_300_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_400_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_400_italic.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_400_italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_400_italic.woff
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_400_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_400_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_400_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_400_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_500_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_500_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_500_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_500_normal.woff
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_700_normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_700_normal.ttf
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_700_normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/fonts/Roboto_700_normal.woff
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-16x16.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-16x16.ico
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-32x32.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-32x32.ico
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/client/public/img/icons/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/mstile-144x144.png
--------------------------------------------------------------------------------
/client/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/client/public/img/icons/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/mstile-310x150.png
--------------------------------------------------------------------------------
/client/public/img/icons/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/mstile-310x310.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/public/img/icons/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/favicon-196x196.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-144x144.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-256x256.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-36x36.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-384x384.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-48x48.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-72x72.png
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-96x96.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/client/public/img/icons/android-chrome-1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/android-chrome-1024x1024.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/client/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/client/public/img/adamant-logo-transparent-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/adamant-logo-transparent-512x512.png
--------------------------------------------------------------------------------
/client/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adamant-im/adamant-2fa/HEAD/client/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/server/component-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "loopback-component-explorer": {
3 | "mountPath": "/explorer",
4 | "generateOperationScopedModels": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/assets/app.styl:
--------------------------------------------------------------------------------
1 | .application, .title
2 | color #444
3 | font-family 'Exo 2', sans-serif !important
4 |
5 | body
6 | overflow-x hidden
7 | html
8 | overflow-y auto
9 |
--------------------------------------------------------------------------------
/server/middleware.development.json:
--------------------------------------------------------------------------------
1 | {
2 | "final:after": {
3 | "strong-error-handler": {
4 | "params": {
5 | "debug": true,
6 | "log": false
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/tests/unit/example.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import App from '@/App.vue'
3 |
4 | test('App should work', () => {
5 | const wrapper = shallowMount(App)
6 | expect(wrapper.text()).toMatch('Welcome to Your Vue.js App')
7 | })
8 |
--------------------------------------------------------------------------------
/server/middleware/error-handler.js:
--------------------------------------------------------------------------------
1 | const logger = require('../../helpers/logger');
2 |
3 | module.exports = function() {
4 | return function(err, req, res, next) {
5 | logger.error(`Request ${req.method} ${req.url} failed: ${err}`);
6 | next(err);
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.csv
2 | *.dat
3 | *.iml
4 | *.log
5 | *.out
6 | *.pid
7 | *.seed
8 | *.sublime-*
9 | *.swo
10 | *.swp
11 | *.tgz
12 | *.xml
13 | .DS_Store
14 | .idea
15 | .project
16 | .strong-pm
17 | coverage
18 | node_modules
19 | npm-debug.log
20 | logs
21 | .vscode
22 | config.json
23 | config.2fa.json
24 |
--------------------------------------------------------------------------------
/server/boot/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(server) {
4 | // Install a `/` route that returns server status
5 | // eslint-disable-next-line new-cap
6 | const router = server.loopback.Router();
7 | router.get('/', server.loopback.status());
8 | server.use(router);
9 | };
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | /tests/e2e/reports/
6 | selenium-debug.log
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw*
25 |
--------------------------------------------------------------------------------
/client/vue.config.js:
--------------------------------------------------------------------------------
1 | // const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin')
2 |
3 | module.exports = {
4 | /* configureWebpack: {
5 | plugins: [
6 | new VuetifyLoaderPlugin()
7 | ]
8 | }, */
9 | pluginOptions: {
10 | i18n: {
11 | locale: 'ru',
12 | fallbackLocale: 'en',
13 | localeDir: 'locales',
14 | enableInSFC: true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/boot/authentication.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function enableAuthentication(server) {
4 | // Removing email requirement
5 | delete server.models.Account.validations.email;
6 | // Enable authentication
7 | server.enableAuth({
8 | // Let LoopBack take care of attaching any built-in models required by the access control
9 | datasource: 'postgresql',
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/server/datasources.json:
--------------------------------------------------------------------------------
1 | {
2 | "db": {
3 | "name": "db",
4 | "connector": "memory"
5 | },
6 | "postgresql": {
7 | "host": "127.0.0.1",
8 | "port": 5432,
9 | "url": "postgres://adamant-2fa:password@localhost/adamant-2fa",
10 | "database": "adamant-2fa",
11 | "password": "password",
12 | "name": "postgresql",
13 | "debug": true,
14 | "user": "adamant-2fa",
15 | "connector": "postgresql"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/tests/e2e/specs/test.js:
--------------------------------------------------------------------------------
1 | // For authoring Nightwatch tests, see
2 | // http://nightwatchjs.org/guide#usage
3 |
4 | module.exports = {
5 | 'default e2e tests': browser => {
6 | browser
7 | .url(process.env.VUE_DEV_SERVER_URL)
8 | .waitForElementVisible('#app', 5000)
9 | .assert.elementPresent('.hello')
10 | .assert.containsText('h1', 'Welcome to Your Vue.js App')
11 | .assert.elementCount('img', 1)
12 | .end()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/server/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "restApiRoot": "/api",
3 | "host": "0.0.0.0",
4 | "port": 3000,
5 | "remoting": {
6 | "context": false,
7 | "rest": {
8 | "handleErrors": false,
9 | "normalizeHttpPath": false,
10 | "xml": false
11 | },
12 | "json": {
13 | "strict": false,
14 | "limit": "100kb"
15 | },
16 | "urlencoded": {
17 | "extended": true,
18 | "limit": "100kb"
19 | },
20 | "cors": false
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | 'standard'
9 | ],
10 | rules: {
11 | 'max-len': [
12 | 'error', { code: 100 }
13 | ],
14 | 'no-console': 'off',
15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
16 | 'vue/multi-word-component-names': 'off'
17 | },
18 | parserOptions: {
19 | parser: '@babel/eslint-parser'
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/common/models/test/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "restApiRoot": "/api",
3 | "host": "0.0.0.0",
4 | "port": 3000,
5 | "remoting": {
6 | "context": false,
7 | "rest": {
8 | "handleErrors": false,
9 | "normalizeHttpPath": false,
10 | "xml": false
11 | },
12 | "json": {
13 | "strict": false,
14 | "limit": "100kb"
15 | },
16 | "urlencoded": {
17 | "extended": true,
18 | "limit": "100kb"
19 | },
20 | "cors": false,
21 | "handleErrors": false
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "2FA ADAMANT Demo",
3 | "short_name": "2FA ADAMANT Demo",
4 | "icons": [
5 | {
6 | "src": "./img/icons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "./img/icons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "./index.html",
17 | "display": "standalone",
18 | "background_color": "#000000",
19 | "theme_color": "#4DBA87"
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/main.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import Vue from 'vue'
3 | import VueAxios from 'vue-axios'
4 |
5 | import './plugins/vuetify'
6 | import './registerServiceWorker'
7 | import '@/assets/app.styl'
8 |
9 | import App from './App.vue'
10 | import i18n from './plugins/i18n'
11 | import router from './router'
12 | import store from './store'
13 |
14 | Vue.use(VueAxios, axios)
15 |
16 | Vue.config.productionTip = false
17 |
18 | new Vue({
19 | components: { App },
20 | i18n,
21 | render: createElement => createElement(App),
22 | router,
23 | store,
24 | template: ''
25 | }).$mount('#app')
26 |
--------------------------------------------------------------------------------
/client/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue'
7 | ],
8 | transform: {
9 | '^.+\\.vue$': 'vue-jest',
10 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
11 | '^.+\\.jsx?$': 'babel-jest'
12 | },
13 | moduleNameMapper: {
14 | '^@/(.*)$': '/src/$1'
15 | },
16 | snapshotSerializers: [
17 | 'jest-serializer-vue'
18 | ],
19 | testMatch: [
20 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
21 | ],
22 | testURL: 'http://localhost/'
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify/lib'
3 | import 'vuetify/src/stylus/app.styl'
4 | import 'vuetify/dist/vuetify.min.css'
5 |
6 | import 'material-design-icons-iconfont/dist/material-design-icons.css'
7 | import 'roboto-fontface/css/roboto/roboto-fontface.css'
8 | import '@mdi/font/css/materialdesignicons.css'
9 |
10 | Vue.use(Vuetify, {
11 | customProperties: true,
12 | iconfont: 'mdi',
13 | theme: {
14 | primary: '#DDD',
15 | secondary: '#444',
16 | accent: '#82B1FF',
17 | error: '#FF5252',
18 | info: '#2196F3',
19 | success: '#4CAF50',
20 | warning: '#FFC107'
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es2021: true,
5 | browser: true,
6 | node: true,
7 | 'jest/globals': true,
8 | },
9 | extends: ['eslint:recommended', 'google'],
10 | plugins: ['jest'],
11 | parserOptions: {
12 | ecmaVersion: 12,
13 | },
14 | rules: {
15 | 'max-len': [
16 | 'error',
17 | {
18 | code: 200,
19 | ignoreTrailingComments: true,
20 | ignoreUrls: true,
21 | ignoreStrings: true,
22 | ignoreTemplateLiterals: true,
23 | ignoreRegExpLiterals: true,
24 | },
25 | ],
26 | 'require-jsdoc': 'off',
27 | 'quote-props': 'off',
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/helpers/test/constants.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-control-regex */
2 | module.exports = {
3 | LOG_FORMAT_REGEX: /^(\x1b)(\[34m)(log)(\|)(2)(0)([0-3])([0-9])-([0-1])([0-9])-([0-3])([0-9])\s([0-2])([0-9]):([0-5])([0-9]):([0-5])([0-9])(\x1b)(\[0m)/,
4 | INFO_FORMAT_REGEX: /^(\x1b)(\[32m)(info)(\|)(2)(0)([0-3])([0-9])-([0-1])([0-9])-([0-3])([0-9])\s([0-2])([0-9]):([0-5])([0-9]):([0-5])([0-9])(\x1b)(\[0m)/,
5 | WARN_FORMAT_REGEX: /^(\x1b)(\[33m)(warn)(\|)(2)(0)([0-3])([0-9])-([0-1])([0-9])-([0-3])([0-9])\s([0-2])([0-9]):([0-5])([0-9]):([0-5])([0-9])(\x1b)(\[0m)/,
6 | ERROR_FORMAT_REGEX: /^(\x1b)(\[31m)(error)(\|)(2)(0)([0-3])([0-9])-([0-1])([0-9])-([0-3])([0-9])\s([0-2])([0-9]):([0-5])([0-9]):([0-5])([0-9])(\x1b)(\[0m)/,
7 | };
8 |
--------------------------------------------------------------------------------
/client/tests/e2e/custom-assertions/elementCount.js:
--------------------------------------------------------------------------------
1 | // A custom Nightwatch assertion.
2 | // The assertion name is the filename.
3 | // Example usage:
4 | //
5 | // browser.assert.elementCount(selector, count)
6 | //
7 | // For more information on custom assertions see:
8 | // http://nightwatchjs.org/guide#writing-custom-assertions
9 |
10 | exports.assertion = function elementCount (selector, count) {
11 | this.message = `Testing if element <${selector}> has count: ${count}`
12 | this.expected = count
13 | this.pass = val => val === count
14 | this.value = res => res.value
15 | function evaluator (_selector) {
16 | return document.querySelectorAll(_selector).length
17 | }
18 | this.command = cb => this.api.execute(evaluator, [selector], cb)
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/components/ScreenLocker.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
17 |
18 |
19 |
20 |
21 |
38 |
39 |
43 |
--------------------------------------------------------------------------------
/client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from 'register-service-worker'
4 |
5 | if (process.env.NODE_ENV === 'production') {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready () {
8 | console.log(
9 | 'App is being served from cache by a service worker.\n' +
10 | 'For more details, visit https://goo.gl/AFskqB'
11 | )
12 | },
13 | cached () {
14 | console.log('Content has been cached for offline use.')
15 | },
16 | updated () {
17 | console.log('New content is available; please refresh.')
18 | },
19 | offline () {
20 | console.log('No internet connection found. App is running in offline mode.')
21 | },
22 | error (error) {
23 | console.error('Error during service worker registration:', error)
24 | }
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/server/create-lb-tables.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @url https://loopback.io/doc/en/lb3/Creating-a-database-schema-from-models
3 | * #creating-database-tables-for-built-in-models
4 | * @url https://loopback.io/doc/en/lb3/Attaching-models-to-data-sources.html
5 | */
6 | 'use strict';
7 |
8 | const server = require('./server');
9 | const logger = require('../helpers/logger');
10 |
11 | const ds = server.dataSources.postgresql;
12 | const lbTables = ['Account', 'User', 'AccessToken', 'ACL', 'RoleMapping', 'Role'];
13 | ds.automigrate(lbTables, function(error) {
14 | if (error) throw error;
15 | logger.info(`Loopback tables [${lbTables}] created in ${ds.adapter.name}`);
16 | server.models.Role.create({
17 | description: 'Indicates that user authorized to access his account',
18 | name: 'authorized',
19 | }, function(error, role) {
20 | if (error) throw error;
21 | logger.info(`Created role: ${JSON.stringify(role)}`);
22 |
23 | ds.disconnect();
24 | logger.info('Finished');
25 | process.exit();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const loopback = require('loopback');
4 | const boot = require('loopback-boot');
5 |
6 | const logger = require('../helpers/logger');
7 |
8 | const app = module.exports = loopback();
9 |
10 | app.start = function() {
11 | // start the web server
12 | return app.listen(function() {
13 | app.emit('started');
14 | const baseUrl = app.get('url').replace(/\/$/, '');
15 | logger.info(`Web server is listening at ${baseUrl}`);
16 | if (app.get('loopback-component-explorer')) {
17 | const explorerPath = app.get('loopback-component-explorer').mountPath;
18 | logger.info(`Browse app REST API at ${baseUrl}${explorerPath}`);
19 | }
20 | });
21 | };
22 |
23 | // Bootstrap the application, configure models, datasources and middleware.
24 | // Sub-apps like REST API are mounted via boot scripts.
25 | boot(app, __dirname, function(err) {
26 | if (err) throw err;
27 |
28 | // start the server if `$ node server.js`
29 | if (require.main === module) app.start();
30 | });
31 |
--------------------------------------------------------------------------------
/common/models/test/middleware.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "initial:before": {
4 | "loopback#favicon": {}
5 | },
6 | "initial": {
7 | "compression": {},
8 | "cors": {
9 | "params": {
10 | "origin": true,
11 | "credentials": true,
12 | "maxAge": 86400
13 | }
14 | },
15 | "helmet#xssFilter": {},
16 | "helmet#frameguard": {
17 | "params": {
18 | "action": "deny"
19 | }
20 | },
21 | "helmet#hsts": {
22 | "params": {
23 | "maxAge": 0,
24 | "includeSubDomains": true
25 | }
26 | },
27 | "helmet#hidePoweredBy": {},
28 | "helmet#ieNoOpen": {},
29 | "helmet#noSniff": {}
30 | },
31 | "session": {},
32 | "auth": {
33 | "loopback#token": {
34 | "params": {
35 | "currentUserLiteral": "user"
36 | }
37 | }
38 | },
39 | "parse": {},
40 | "routes": {
41 | "loopback#rest": {
42 | "paths": [
43 | "${restApiRoot}"
44 | ]
45 | }
46 | },
47 | "files": {},
48 | "final": {
49 | "loopback#urlNotFound": {}
50 | },
51 | "final:after": {
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/server/model-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "sources": [
4 | "loopback/common/models",
5 | "loopback/server/models",
6 | "../common/models",
7 | "./models"
8 | ],
9 | "mixins": [
10 | "loopback/common/mixins",
11 | "loopback/server/mixins",
12 | "../common/mixins",
13 | "./mixins"
14 | ]
15 | },
16 | "User": {
17 | "dataSource": "postgresql"
18 | },
19 | "AccessToken": {
20 | "dataSource": "postgresql",
21 | "public": false,
22 | "relations": {
23 | "account": {
24 | "type": "belongsTo",
25 | "model": "Account",
26 | "foreignKey": "userId"
27 | }
28 | }
29 | },
30 | "ACL": {
31 | "dataSource": "postgresql",
32 | "public": false
33 | },
34 | "RoleMapping": {
35 | "dataSource": "postgresql",
36 | "public": false,
37 | "options": {
38 | "strictObjectIDCoercion": true
39 | }
40 | },
41 | "Role": {
42 | "dataSource": "postgresql",
43 | "public": false
44 | },
45 | "Account": {
46 | "dataSource": "postgresql",
47 | "public": true
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config.default.json:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | The app will use this ADM passPhrase to send 2FA codes.
4 | Create it at https://msg.adamant.im. Make sure you've received free ADM tokens.
5 | ADM address will correspond this passPhrase.
6 | **/
7 | "passPhrase": "distance expect praise frequent..",
8 |
9 | /** Choose 'mainnet' or 'testnet' **/
10 | "network": "mainnet",
11 |
12 | /** Additionally, you can specify what ADM nodes to use **/
13 | "networks": {
14 | "testnet": {
15 | "nodes": [
16 | {
17 | "ip": "127.0.0.1",
18 | "protocol": "http",
19 | "port": 36667
20 | }
21 | ]
22 | },
23 | "mainnet": {
24 | "nodes": [
25 | {
26 | "ip": "clown.adamant.im",
27 | "protocol": "https",
28 | "port": ""
29 | },
30 | {
31 | "ip": "lake.adamant.im",
32 | "protocol": "https",
33 | "port": ""
34 | },
35 | {
36 | "ip": "endless.adamant.im",
37 | "protocol": "https",
38 | "port": ""
39 | }
40 | ]
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/middleware.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial:before": {
3 | "loopback#favicon": {}
4 | },
5 | "initial": {
6 | "compression": {},
7 | "cors": {
8 | "params": {
9 | "origin": true,
10 | "credentials": true,
11 | "maxAge": 86400
12 | }
13 | },
14 | "helmet#xssFilter": {},
15 | "helmet#frameguard": {
16 | "params": {
17 | "action": "deny"
18 | }
19 | },
20 | "helmet#hsts": {
21 | "params": {
22 | "maxAge": 0,
23 | "includeSubDomains": true
24 | }
25 | },
26 | "helmet#hidePoweredBy": {},
27 | "helmet#ieNoOpen": {},
28 | "helmet#noSniff": {}
29 | },
30 | "session": {},
31 | "auth": {
32 | "loopback#token": {
33 | "params": {
34 | "currentUserLiteral": "user"
35 | }
36 | }
37 | },
38 | "parse": {},
39 | "routes": {
40 | "loopback#rest": {
41 | "paths": [
42 | "${restApiRoot}"
43 | ]
44 | }
45 | },
46 | "files": {},
47 | "final": {
48 | "loopback#urlNotFound": {}
49 | },
50 | "final:after": {
51 | "./middleware/error-handler.js": {},
52 | "strong-error-handler": {}
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/components/SnackbarNote.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | {{ note }}
8 |
9 |
10 |
11 |
48 |
--------------------------------------------------------------------------------
/client/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const history = require('connect-history-api-fallback')
3 | const path = require('path')
4 | const morgan = require('morgan')
5 | const split = require('split')
6 | const compression = require('compression')
7 | const logger = require('../helpers/logger')
8 |
9 | const app = express()
10 |
11 | const port = process.env.PORT || 8080
12 |
13 | app.use(history())
14 |
15 | app.use((req, res, next) => {
16 | res.setHeader('X-Frame-Options', 'DENY')
17 | res.setHeader('X-Content-Type-Options', 'nosniff')
18 | res.setHeader('X-XSS-Protection', '1; mode=block')
19 | res.header('Access-Control-Allow-Origin', '*')
20 | res.header('Access-Control-Allow-Methods', 'GET')
21 | res.header('Access-Control-Allow-Headers', 'Content-Type')
22 |
23 | next()
24 | })
25 |
26 | app.use(
27 | morgan('combined', {
28 | skip: (req, res) => {
29 | return parseInt(res.statusCode) < 400
30 | },
31 | stream: split().on('data', (data) => {
32 | logger.error(`Request failed: ${data}`)
33 | })
34 | })
35 | )
36 |
37 | app.use(
38 | morgan('combined', {
39 | skip: (req, res) => {
40 | return parseInt(res.statusCode) >= 400
41 | },
42 | stream: split().on('data', (data) => {
43 | logger.log(`Request successful: ${data}`)
44 | })
45 | })
46 | )
47 |
48 | app.use(compression())
49 |
50 | app.use(express.static(path.join(__dirname, '/dist')))
51 |
52 | app.get('*', (request, response) => {
53 | response.sendFile(path.resolve(path.join(__dirname, '/dist'), 'index.html'))
54 | })
55 |
56 | app.listen(port)
57 | logger.info(`Client app started on port ${port}`)
58 |
--------------------------------------------------------------------------------
/client/src/plugins/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 |
4 | Vue.use(VueI18n)
5 |
6 | function loadLocaleMessages () {
7 | const locales = require.context('../locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
8 | const messages = {}
9 | locales.keys().forEach(key => {
10 | const matched = key.match(/([A-Za-z0-9-_]+)\./i)
11 | if (matched && matched.length > 1) {
12 | const locale = matched[1]
13 | messages[locale] = locales(key)
14 | }
15 | })
16 | return messages
17 | }
18 |
19 | export default new VueI18n({
20 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
21 | locale: process.env.VUE_APP_I18N_LOCALE || 'en',
22 | messages: loadLocaleMessages(),
23 | pluralizationRules: {
24 | /** Key - language to use the rule for, 'ru', in this case */
25 | /** Value - function
26 | * @param choice {number} a choice index given by the input to $tc:
27 | * `$tc('path.to.rule', choiceIndex)`
28 | * @param choicesLength {number} an overall amount of available choices
29 | * @returns a final choice index to select plural word by
30 | */
31 | ru: function (choice, choicesLength) {
32 | // this === VueI18n instance, so the locale property also exists here
33 | if (choice === 0) {
34 | return 0
35 | }
36 | const teen = choice > 10 && choice < 20
37 | const endsWithOne = choice % 10 === 1
38 | if (choicesLength < 4) {
39 | return (!teen && endsWithOne) ? 1 : 2
40 | }
41 | if (!teen && endsWithOne) {
42 | return 1
43 | }
44 | if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
45 | return 2
46 | }
47 | return (choicesLength < 4) ? 2 : 3
48 | }
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/client/src/components/LanguageSwitcher.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
19 |
59 |
60 |
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adamant-2fa",
3 | "version": "2.0.0",
4 | "description": "ADAMANT 2FA demo app to deliver one-time passwords (OTP) using blockchain",
5 | "main": "server/server.js",
6 | "keywords": [
7 | "adm",
8 | "adamant",
9 | "blockchain",
10 | "messenger",
11 | "2fa",
12 | "security",
13 | "sms",
14 | "crypto",
15 | "cryptocurrency",
16 | "encryption",
17 | "2-factor",
18 | "OTP",
19 | "authentication"
20 | ],
21 | "author": "ADAMANT Devs (https://adamant.im)",
22 | "license": "GPL-3.0",
23 | "engines": {
24 | "node": ">=16.15.0"
25 | },
26 | "scripts": {
27 | "lint": "eslint .",
28 | "start": "node .",
29 | "posttest": "npm run lint",
30 | "test": "jest helpers common/models --forceExit"
31 | },
32 | "dependencies": {
33 | "adamant-console": "^2.1.0",
34 | "compression": "^1.7.4",
35 | "cors": "^2.8.5",
36 | "eslint-plugin-jest": "^27.1.6",
37 | "helmet": "^6.0.1",
38 | "jsonminify": "^0.4.2",
39 | "loopback": "^3.28.0",
40 | "loopback-boot": "^2.28.0",
41 | "loopback-component-explorer": "6.5.1",
42 | "loopback-connector-postgresql": "6.0.0",
43 | "serve-favicon": "^2.5.0",
44 | "speakeasy": "^2.0.0",
45 | "strong-error-handler": "^4.0.1"
46 | },
47 | "devDependencies": {
48 | "axios": "^1.2.1",
49 | "eslint": "^8.29.0",
50 | "eslint-config-google": "^0.14.0",
51 | "eslint-config-loopback": "^13.1.0",
52 | "jest": "^29.3.1"
53 | },
54 | "overrides": {
55 | "node-fetch": "2.6.7",
56 | "ejs": "^3.1.7",
57 | "underscore": "^1.12.1",
58 | "@braintree/sanitize-url": "^6.0.0"
59 | },
60 | "repository": {
61 | "type": "git",
62 | "url": "git+https://github.com/Adamant-im/adamant-2fa.git"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Loading...
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | <%= htmlWebpackPlugin.options.basePath %>
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/server/boot/role-resolver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(app) {
4 | const Role = app.models.Role;
5 | const RoleMapping = app.models.RoleMapping;
6 | // User asks authorization to access account
7 | Role.registerResolver('authorized', function(role, ctx, next) {
8 | ctx.model.findById(ctx.modelId, function(error, account) {
9 | if (error) return next(error);
10 | if (ctx.accessToken.userId) {
11 | // User had authenticated
12 | if (account.se2faEnabled) {
13 | // Check that user had passed 2FA verification
14 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
15 | if (error) return next(error);
16 | RoleMapping.findOne({
17 | principalType: 'USER',
18 | principalId: account.id,
19 | roleId: role.getId(),
20 | }, (error, roleMapping) => {
21 | if (error) return next(error);
22 | if (roleMapping) {
23 | // User had verified, allow
24 | next(null, true);
25 | } else {
26 | // User had not verified, deny
27 | next(null, false);
28 | }
29 | });
30 | });
31 | } else next(null, true); // 2FA disabled, allow
32 | } else {
33 | // User unauthenticated, deny
34 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
35 | if (error) return next(error);
36 | RoleMapping.findOne({
37 | principalType: 'USER',
38 | principalId: account.id,
39 | roleId: role.getId(),
40 | }, (error, roleMapping) => {
41 | if (error) return next(error);
42 | if (roleMapping) {
43 | // Revoke previously assigned role
44 | roleMapping.destroy((error) => {
45 | if (error) return next(error);
46 | next(null, false);
47 | });
48 | } else next(null, false);
49 | });
50 | });
51 | }
52 | });
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
56 |
57 |
68 |
--------------------------------------------------------------------------------
/client/src/components/NavigationMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
20 |
21 | mdi-cog
22 |
23 |
27 |
28 | mdi-logout-variant
29 |
30 |
31 |
32 |
33 |
34 |
35 |
64 |
65 |
86 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "2.0.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "test:e2e": "vue-cli-service test:e2e",
10 | "test:unit": "vue-cli-service test:unit",
11 | "serve-build": "node server.js"
12 | },
13 | "dependencies": {
14 | "@mdi/font": "^7.1.96",
15 | "@vue/babel-preset-app": "^5.0.8",
16 | "axios": "^1.2.1",
17 | "compression": "^1.7.4",
18 | "connect-history-api-fallback": "^2.0.0",
19 | "express": "^4.18.2",
20 | "material-design-icons-iconfont": "^6.7.0",
21 | "morgan": "^1.10.0",
22 | "register-service-worker": "^1.7.2",
23 | "roboto-fontface": "^0.10.0",
24 | "split": "^1.0.1",
25 | "vue": "^2.5.17",
26 | "vue-axios": "^3.5.2",
27 | "vue-i18n": "^8.0.0",
28 | "vue-router": "^3.0.1",
29 | "vuetify": "^1.3.0",
30 | "vuex": "^3.0.1",
31 | "vuex-persistedstate": "^2.5.4"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.20.5",
35 | "@babel/eslint-parser": "^7.19.1",
36 | "@kazupon/vue-i18n-loader": "^0.3.0",
37 | "@vue/cli-plugin-babel": "^5.0.8",
38 | "@vue/cli-plugin-e2e-nightwatch": "^5.0.8",
39 | "@vue/cli-plugin-eslint": "^5.0.8",
40 | "@vue/cli-plugin-pwa": "^5.0.8",
41 | "@vue/cli-plugin-unit-jest": "5.0.8",
42 | "@vue/cli-service": "^5.0.8",
43 | "@vue/eslint-config-standard": "^8.0.1",
44 | "@vue/test-utils": "^2.2.6",
45 | "babel-core": "7.0.0-bridge.0",
46 | "babel-jest": "^29.3.1",
47 | "babel-plugin-polyfill-corejs2": "^0.3.3",
48 | "babel-plugin-polyfill-corejs3": "^0.6.0",
49 | "babel-plugin-polyfill-regenerator": "^0.4.1",
50 | "chromedriver": "*",
51 | "eslint": "8.29.0",
52 | "eslint-plugin-import": "^2.26.0",
53 | "eslint-plugin-n": "^15.6.0",
54 | "eslint-plugin-promise": "^6.1.1",
55 | "eslint-plugin-vue": "^9.8.0",
56 | "jest": "^27.1.0",
57 | "lint-staged": "^13.1.0",
58 | "node-sass": "^7.0.1",
59 | "sass-loader": "^13.2.0",
60 | "stylus": "^0.59.0",
61 | "stylus-loader": "^7.1.0",
62 | "vue-cli-plugin-i18n": "^0.5.1",
63 | "vue-cli-plugin-vuetify": "^0.4.6",
64 | "vue-jest": "^3.0.7",
65 | "vue-template-compiler": "^2.7.14",
66 | "vuetify-loader": "^1.9.2",
67 | "webpack": "^5.75.0"
68 | },
69 | "resolutions": {
70 | "loader-utils": "^2.0.4",
71 | "minimatch": "^3.0.5",
72 | "decode-uri-component": "^0.2.1"
73 | },
74 | "gitHooks": {
75 | "pre-commit": "lint-staged"
76 | },
77 | "lint-staged": {
78 | "*.js": [
79 | "vue-cli-service lint",
80 | "git add"
81 | ],
82 | "*.vue": [
83 | "vue-cli-service lint",
84 | "git add"
85 | ]
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/client/src/router.js:
--------------------------------------------------------------------------------
1 | import Router from 'vue-router'
2 | import Vue from 'vue'
3 |
4 | import Login from '@/views/Login'
5 | import Signup from '@/views/Signup'
6 | // import Settings from '@/views/Settings'
7 | import Verify from '@/views/Verify'
8 |
9 | import store from './store'
10 |
11 | Vue.use(Router)
12 |
13 | const router = new Router({
14 | base: process.env.BASE_URL,
15 | mode: 'history',
16 | routes: [
17 | {
18 | component: Login,
19 | name: 'login',
20 | path: '/login',
21 | props: true
22 | },
23 | {
24 | component: () => import(/* webpackChunkName: "settings" */ '@/views/Settings.vue'),
25 | // component: Settings,
26 | meta: { authorization: true },
27 | name: 'settings',
28 | path: '/settings'
29 | },
30 | {
31 | component: Signup,
32 | name: 'signup',
33 | path: '/signup'
34 | },
35 | {
36 | component: Verify,
37 | meta: { authentication: true },
38 | name: 'verify',
39 | path: '/verify'
40 | },
41 | {
42 | alias: '/signup',
43 | path: '*'
44 | }
45 | ]
46 | })
47 |
48 | router.beforeEach((to, from, next) => {
49 | const authentication = to.matched.some(route => route.meta.authentication)
50 | const authorization = to.matched.some(route => route.meta.authorization)
51 | const account = store.state.account
52 | const session = store.state.session
53 | if (authorization) {
54 | // Authorization required - settings
55 | if (session.se2faVerified) {
56 | next()
57 | } else if (session.created && account.se2faEnabled) {
58 | next('/verify')
59 | } else if (session.lastSeen) {
60 | next('/login')
61 | } else {
62 | next('/signup')
63 | }
64 | } else if (authentication) {
65 | // Authentication required - settings, verify
66 | if (session.se2faVerified) {
67 | next('/settings')
68 | } else if (session.created && account.se2faEnabled) {
69 | next()
70 | } else if (session.lastSeen) {
71 | next('/login')
72 | } else {
73 | next('/signup')
74 | }
75 | } else if (to.name) {
76 | // No permission required
77 | /* if (session.se2faVerified) {
78 | next('/settings')
79 | } else if (session.created && account.se2faEnabled) {
80 | next('/verify')
81 | } else {
82 | next()
83 | } */
84 | next()
85 | } else {
86 | // Path undefined
87 | if (session.se2faVerified) {
88 | next('/settings')
89 | } else if (session.created && account.se2faEnabled) {
90 | next('/verify')
91 | } else if (session.lastSeen) {
92 | next('/login')
93 | } else {
94 | next('/signup')
95 | }
96 | }
97 | })
98 |
99 | export default router
100 |
--------------------------------------------------------------------------------
/helpers/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fs = require('fs');
3 | if (!fs.existsSync('./logs')) {
4 | fs.mkdirSync('./logs');
5 | }
6 |
7 | const infoStr = fs.createWriteStream(`./logs/${date()}.log`, {
8 | flags: 'a',
9 | });
10 |
11 | infoStr.write(`\n\n[The app started] _________________${fullTime()}_________________\n`);
12 |
13 | const logger = {
14 | errorLevel: 'log',
15 | logger: console,
16 |
17 | initLogger(errorLevel, log) {
18 | if (errorLevel) {
19 | this.errorLevel = errorLevel;
20 | }
21 |
22 | if (log) {
23 | this.logger = log;
24 | }
25 | },
26 | error(str) {
27 | if (['error', 'warn', 'info', 'log'].includes(this.errorLevel)) {
28 | this.logger.log('\x1b[31m', 'error|' + fullTime(), '\x1b[0m', str);
29 | infoStr.write(`\n ` + 'error|' + fullTime() + '|' + str);
30 | }
31 | },
32 | warn(str) {
33 | if (['warn', 'info', 'log'].includes(this.errorLevel)) {
34 | this.logger.log('\x1b[33m', 'warn|' + fullTime(), '\x1b[0m', str);
35 | infoStr.write(`\n ` + 'warn|' + fullTime() + '|' + str);
36 | }
37 | },
38 | info(str) {
39 | if (['info', 'log'].includes(this.errorLevel)) {
40 | this.logger.log('\x1b[32m', 'info|' + fullTime(), '\x1b[0m', str);
41 | infoStr.write(`\n ` + 'info|' + fullTime() + '|' + str);
42 | }
43 | },
44 | log(str) {
45 | if (this.errorLevel === 'log') {
46 | this.logger.log('\x1b[34m', 'log|' + fullTime(), '\x1b[0m', str);
47 | infoStr.write(`\n ` + 'log|[' + fullTime() + '|' + str);
48 | }
49 | },
50 | };
51 |
52 | function time() {
53 | return formatDate(Date.now()).hh_mm_ss;
54 | }
55 |
56 | function date() {
57 | return formatDate(Date.now()).YYYY_MM_DD;
58 | }
59 |
60 | function fullTime() {
61 | return date() + ' ' + time();
62 | }
63 |
64 | /**
65 | * Formats unix timestamp to string
66 | * @param {number} timestamp Timestamp to format
67 | * @return {object} Contains different formatted strings
68 | */
69 | function formatDate(timestamp) {
70 | if (!timestamp) return false;
71 | const formattedDate = {};
72 | const dateObject = new Date(timestamp);
73 | formattedDate.year = dateObject.getFullYear();
74 | formattedDate.month = ('0' + (dateObject.getMonth() + 1)).slice(-2);
75 | formattedDate.date = ('0' + dateObject.getDate()).slice(-2);
76 | formattedDate.hours = ('0' + dateObject.getHours()).slice(-2);
77 | formattedDate.minutes = ('0' + dateObject.getMinutes()).slice(-2);
78 | formattedDate.seconds = ('0' + dateObject.getSeconds()).slice(-2);
79 | formattedDate.YYYY_MM_DD = formattedDate.year + '-' + formattedDate.month + '-' + formattedDate.date;
80 | formattedDate.YYYY_MM_DD_hh_mm = formattedDate.year + '-' + formattedDate.month + '-' + formattedDate.date + ' ' + formattedDate.hours + ':' + formattedDate.minutes;
81 | formattedDate.hh_mm_ss = formattedDate.hours + ':' + formattedDate.minutes + ':' + formattedDate.seconds;
82 | return formattedDate;
83 | }
84 |
85 | module.exports = logger;
86 |
--------------------------------------------------------------------------------
/client/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "204": {
3 | "logout": "You've logged out"
4 | },
5 | "2fa": "2FA",
6 | "2faCode": "Enter 2FA code",
7 | "2faDisabled": "2FA is disabled now",
8 | "2faEnabled": "2FA is enabled. Re-login now",
9 | "2faNotValid": "| Code is invalid: 1 attempt left | Code is invalid: {n} attempts left",
10 | "2faRequest": "Enter 2FA code received in U…{address}",
11 | "2faSentWithTx": "2FA code sent with Tx ID: {id}",
12 | "400": {
13 | "login": "Enter username",
14 | "signup": "Enter username"
15 | },
16 | "401": {
17 | "login": "Wrong username or password",
18 | "logout": "Session expired"
19 | },
20 | "422": {
21 | "signup": "Username is already taken. Choose another one"
22 | },
23 | "500": {
24 | "login": "Error occurred on server. View logs"
25 | },
26 | "503": {
27 | "login": "Server is not available. Try again later",
28 | "logout": "Server is not available. Try again later",
29 | "signup": "Server is not available. Try again later"
30 | },
31 | "sentMessageErrors": {
32 | "WRONG_PASSPHRASE": "Error: Wrong passPhrase in the config file",
33 | "NOT_ENOUGH_ADM": "Error: Account in the config has no ADM. Get free tokens",
34 | "RECIPIENT_UNINITIALIZED": "Error: This ADAMANT address is not used. Get free tokens",
35 | "NOT_SENT_GENERAL": "Error: Unable to send 2FA code",
36 | "NOT_SENT_NETWORK": "Error: No Internet connection"
37 | },
38 | "documentTitle": "2FA demo — ADAMANT",
39 | "headerTitle": "ADAMANT 2FA",
40 | "empty": "",
41 | "enable2fa": "Enable 2FA",
42 | "enter2faCode": "Enter 2FA code you've received",
43 | "enterAdamantAddress": "ADAMANT address to receive 2FA codes",
44 | "general": "General",
45 | "get2faCode": "Get 2FA code",
46 | "language": "Language",
47 | "login": "Login",
48 | "loginSubheader": "One-time passwords decentralized",
49 | "logout": "Logout",
50 | "name": "English",
51 | "password": "Password",
52 | "patternMismatch": {
53 | "adamantAddress": "Wrong ADAMANT address",
54 | "hotp": "Wrong 2FA code",
55 | "hotpEnable": "Wrong 2FA code"
56 | },
57 | "redirectAdamant": {
58 | "inner": "Create in a second",
59 | "outer": "Don't have ADAMANT account yet? {0}"
60 | },
61 | "redirectLogin": "I have an account. Let me in",
62 | "redirectSignup": "Don't have an account? Signup",
63 | "security": "Security",
64 | "settings": "Settings",
65 | "signedUp": "You've signed up. Login now",
66 | "signup": "Signup",
67 | "signupSubheader": "One-time passwords decentralized",
68 | "tooLong": {
69 | "adamantAddress": "Wrong ADAMANT address",
70 | "hotp": "Wrong 2FA code",
71 | "hotpEnable": "Wrong 2FA code"
72 | },
73 | "tooShort": {
74 | "adamantAddress": "Wrong ADAMANT address",
75 | "hotp": "Wrong 2FA code",
76 | "hotpEnable": "Wrong 2FA code",
77 | "password": "Password must be at least 3 characters long",
78 | "username": "Username must be at least 3 characters long"
79 | },
80 | "username": "Username",
81 | "valueMissing": {
82 | "adamantAddress": "Enter ADAMANT address",
83 | "hotp": "Enter 2FA code",
84 | "password": "Enter password",
85 | "username": "Enter username"
86 | },
87 | "verify": "Verify"
88 | }
89 |
--------------------------------------------------------------------------------
/client/src/locales/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "204": {
3 | "logout": "Вы вышли из аккаунта"
4 | },
5 | "2fa": "2FA",
6 | "2faCode": "Введите код 2FA",
7 | "2faDisabled": "2FA отключена",
8 | "2faEnabled": "2FA включена. Перезайдите в аккаунт",
9 | "2faNotValid": "| Неправильный код: осталась 1 попытка | Неправильный код: осталось {n} попытки",
10 | "2faRequest": "Введите код 2FA, полученный в U…{address}",
11 | "2faSentWithTx": "Отправлен код 2FA, Tx ID: {id}",
12 | "400": {
13 | "login": "Укажите имя пользователя",
14 | "signup": "Укажите имя пользователя"
15 | },
16 | "401": {
17 | "login": "Неверные имя пользователя или пароль",
18 | "logout": "Время сессии истекло"
19 | },
20 | "422": {
21 | "signup": "Имя пользователя уже занято. Выберите другое"
22 | },
23 | "500" :{
24 | "login": "На сервере произошла ошибка. Изучите логи сервера"
25 | },
26 | "503": {
27 | "login": "Сервер недоступен. Попробуйте позже",
28 | "logout": "Сервер недоступен. Попробуйте позже",
29 | "signup": "Сервер недоступен. Попробуйте позже"
30 | },
31 | "sentMessageErrors": {
32 | "WRONG_PASSPHRASE": "Ошибка: Неверная пассфраза в конфиге",
33 | "NOT_ENOUGH_ADM": "Ошибка: В конфиге аккаунт с нулевым балансом. Получите бесплатные токены",
34 | "RECIPIENT_UNINITIALIZED": "Ошибка: Этот адрес АДАМАНТа не используется. Получите бесплатные токены",
35 | "NOT_SENT_GENERAL": "Ошибка: Не получилось отправить код 2FA",
36 | "NOT_SENT_NETWORK": "Ошибка: Нет подключения к Интернету"
37 | },
38 | "documentTitle": "Демонстрация 2FA — АДАМАНТ",
39 | "headerTitle": "ADAMANT 2FA",
40 | "empty": "",
41 | "enable2fa": "Включить 2FA",
42 | "enter2faCode": "Введите полученный код",
43 | "enterAdamantAddress": "Адрес АДАМАНТа для получения кодов 2FA",
44 | "general": "Общие",
45 | "get2faCode": "Получить 2FA-код",
46 | "language": "Язык",
47 | "login": "Войти",
48 | "loginSubheader": "Одноразовые пароли на блокчейне",
49 | "logout": "Выйти",
50 | "name": "Русский",
51 | "password": "Пароль",
52 | "patternMismatch": {
53 | "adamantAddress": "Неверный адрес АДАМАНТа",
54 | "hotp": "Неверный код 2FA",
55 | "hotpEnable": "Неверный код 2FA"
56 | },
57 | "redirectAdamant": {
58 | "inner": "Создайте за секунду",
59 | "outer": "Еще нет аккаунта АДАМАНТа? {0}"
60 | },
61 | "redirectLogin": "У меня уже есть аккаунт. Войти",
62 | "redirectSignup": "Нет аккаунта? Создайте новый",
63 | "security": "Безопасность",
64 | "settings": "Настройки",
65 | "signedUp": "Аккаунт создан. Теперь войдите",
66 | "signup": "Создать аккаунт",
67 | "signupSubheader": "Одноразовые пароли на блокчейне",
68 | "tooLong": {
69 | "adamantAddress": "Неверный адрес АДАМАНТа",
70 | "hotp": "Неверный код 2FA",
71 | "hotpEnable": "Неверный код 2FA"
72 | },
73 | "tooShort": {
74 | "adamantAddress": "Неверный адрес АДАМАНТа",
75 | "hotp": "Неверный код 2FA",
76 | "hotpEnable": "Неверный код 2FA",
77 | "password": "Пароль должен быть от 3-х символов",
78 | "username": "Имя пользователя должно быть от 3-х символов"
79 | },
80 | "username": "Имя пользователя",
81 | "valueMissing": {
82 | "adamantAddress": "Введите адрес АДАМАНТа",
83 | "hotp": "Введите код 2FA",
84 | "password": "Введите пароль",
85 | "username": "Введите имя пользователя"
86 | },
87 | "verify": "Проверить"
88 | }
89 |
--------------------------------------------------------------------------------
/client/public/fonts/Roboto.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto';
3 | font-style: italic;
4 | font-weight: 400;
5 | src: url(Roboto_400_italic.eot); /* {{embedded-opentype-gf-url}} */
6 | src: local('☺'),
7 | url(Roboto_400_italic.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
8 | url(Roboto_400_italic.woff) format('woff'), /* http://fonts.gstatic.com/s/roboto/v18/1pO9eUAp8pSF8VnRTP3xnvesZW2xOQ-xsNqO47m55DA.woff */
9 | url(Roboto_400_italic.ttf) format('truetype'), /* http://fonts.gstatic.com/s/roboto/v18/W4wDsBUluyw0tK3tykhXEXYhjbSpvc47ee6xR_80Hnw.ttf */
10 | url(Roboto_400_italic.svg#Roboto_400_italic) format('svg'); /* {{svg-gf-url}} */
11 | }
12 | @font-face {
13 | font-family: 'Roboto';
14 | font-style: normal;
15 | font-weight: 300;
16 | src: url(Roboto_300_normal.eot); /* {{embedded-opentype-gf-url}} */
17 | src: local('☺'),
18 | url(Roboto_300_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
19 | url(Roboto_300_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/roboto/v18/Hgo13k-tfSpn0qi1SFdUfT8E0i7KZn-EPnyo3HZu7kw.woff */
20 | url(Roboto_300_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/roboto/v18/Hgo13k-tfSpn0qi1SFdUfSZ2oysoEQEeKwjgmXLRnTc.ttf */
21 | url(Roboto_300_normal.svg#Roboto_300_normal) format('svg'); /* {{svg-gf-url}} */
22 | }
23 | @font-face {
24 | font-family: 'Roboto';
25 | font-style: normal;
26 | font-weight: 400;
27 | src: url(Roboto_400_normal.eot); /* {{embedded-opentype-gf-url}} */
28 | src: local('☺'),
29 | url(Roboto_400_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
30 | url(Roboto_400_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/roboto/v18/2UX7WLTfW3W8TclTUvlFyQ.woff */
31 | url(Roboto_400_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/roboto/v18/QHD8zigcbDB8aPfIoaupKOvvDin1pK8aKteLpeZ5c0A.ttf */
32 | url(Roboto_400_normal.svg#Roboto_400_normal) format('svg'); /* http://fonts.gstatic.com/l/font?kit=_YZOZaQ9UBZzaxiLBLcgZg&skey=a0a0114a1dcab3ac&v=v18#Roboto */
33 | }
34 | @font-face {
35 | font-family: 'Roboto';
36 | font-style: normal;
37 | font-weight: 500;
38 | src: url(Roboto_500_normal.eot); /* {{embedded-opentype-gf-url}} */
39 | src: local('☺'),
40 | url(Roboto_500_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
41 | url(Roboto_500_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/roboto/v18/RxZJdnzeo3R5zSexge8UUT8E0i7KZn-EPnyo3HZu7kw.woff */
42 | url(Roboto_500_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/roboto/v18/RxZJdnzeo3R5zSexge8UUSZ2oysoEQEeKwjgmXLRnTc.ttf */
43 | url(Roboto_500_normal.svg#Roboto_500_normal) format('svg'); /* {{svg-gf-url}} */
44 | }
45 | @font-face {
46 | font-family: 'Roboto';
47 | font-style: normal;
48 | font-weight: 700;
49 | src: url(Roboto_700_normal.eot); /* {{embedded-opentype-gf-url}} */
50 | src: local('☺'),
51 | url(Roboto_700_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
52 | url(Roboto_700_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/roboto/v18/d-6IYplOFocCacKzxwXSOD8E0i7KZn-EPnyo3HZu7kw.woff */
53 | url(Roboto_700_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/roboto/v18/d-6IYplOFocCacKzxwXSOCZ2oysoEQEeKwjgmXLRnTc.ttf */
54 | url(Roboto_700_normal.svg#Roboto_700_normal) format('svg'); /* {{svg-gf-url}} */
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ADAMANT 2FA Demo Application
2 |
3 | ## What is ADAMANT 2FA
4 |
5 | ADAMANT 2FA is a service to deliver one-time passwords (OTP) to ADAMANT Messenger account.
6 |
7 | It's cheaper, more secure and reliable than SMS. [ADAMANT 2FA advantages](https://medium.com/adamant-im/adamant-is-working-on-a-perfect-2fa-solution-15280b8a3349).
8 |
9 | Live demo is available at [2fa-demo.adamant.im](https://2fa-demo.adamant.im/signup). For details read [Presenting ADAMANT 2FA](https://medium.com/adamant-im/presenting-adamant-2fa-838db2322f7a).
10 |
11 | ## Prerequisites
12 |
13 | * [PostgreSQL](https://www.postgresql.org/download/)
14 |
15 | ## Setup
16 |
17 | Clone repository and install dependencies:
18 |
19 | ``` bash
20 | git clone https://github.com/Adamant-im/adamant-2fa.git
21 | cd adamant-2fa && npm i
22 | cd client && yarn install
23 | cd ../
24 | ```
25 |
26 | Create db-user and 2fa database:
27 |
28 | ```
29 | sudo -u postgres psql
30 | postgres=# CREATE USER "adamant-2fa" WITH PASSWORD 'password';
31 | postgres=# CREATE DATABASE "adamant-2fa" WITH OWNER "adamant-2fa";
32 | ```
33 |
34 | Set up md5 auth method.
35 | Get hba_file path:
36 |
37 | ```
38 | postgres=# SHOW hba_file;
39 | hba_file
40 | -------------------------------------
41 | /usr/local/var/postgres/pg_hba.conf
42 | (1 row)
43 | ```
44 |
45 | Update hba_file and restart postgresql:
46 |
47 | ``` bash
48 | sudo nano /usr/local/var/postgres/pg_hba.conf
49 | local adamant-2fa adamant-2fa md5
50 | sudo service postgresql restart
51 | ```
52 |
53 | Create tables for Loopback models:
54 |
55 | ``` bash
56 | cd server && node create-lb-tables.js
57 | cd ../
58 | ```
59 |
60 | Set up ADAMANT `passPhrase` to send 2fa codes from:
61 |
62 | ```
63 | cp config.default.json config.json
64 | nano config.json
65 | ```
66 |
67 | The 2FA app uses `config.json` file. Enter your ADM passphrase into `passPhrase` field. Make sure this account has ADM to send messages with 2fa codes. [How to create ADM account and get free tokens](https://medium.com/adamant-im/how-to-start-with-a-blockchain-messenger-54d1eb9704e6).
68 |
69 | Note: 2FA demo uses [adamant-console](https://github.com/Adamant-im/adamant-console/) to send 2fa codes. If you have this tool installed separately, make sure it's default config doesn't exist, or set to `mainnet` and correct `passPhrase`.
70 |
71 | Note: If the app doesn't send 2FA codes, debug console shows `{"success":false,"errorMessage":"Wrong 'passPhrase' parameter"}`. Make sure you've created `config.json` in the root directory (where `package.json` located), and you've set `passPhrase` and `network` parameters.
72 |
73 | ## Start
74 |
75 | ### Serve (Dev mode)
76 |
77 | ``` bash
78 | node .
79 | cd client && yarn serve
80 | ```
81 |
82 | ### Build
83 |
84 | ```
85 | cd client && yarn build
86 | yarn serve-build
87 | ```
88 |
89 | ### Launch as process manager process
90 |
91 | We recommend to use a process manager to start the program, f. e. [`pm2`](https://pm2.keymetrics.io/):
92 |
93 | ```
94 | pm2 start ./server/server.js --name 2fa-demo-server
95 | pm2 start ./client/server.js --name 2fa-demo-client
96 | ```
97 |
98 | ## How to connect ADAMANT 2FA to your service
99 |
100 | If you own a service (as email, exchange, financial interface, etc.) and want to add 2FA security for users, connect ADAMANT 2FA. To use ADAMANT 2FA, clone this project and modify client and server parts. Read more: [How to connect ADAMANT 2FA to your business](https://medium.com/adamant-im/go-to-secure-2fa-on-a-blockchain-344500a5f010).
101 |
--------------------------------------------------------------------------------
/client/public/fonts/Exo+2.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Exo 2';
3 | font-style: italic;
4 | font-weight: 400;
5 | src: url('Exo+2_400_italic.eot'); /* {{embedded-opentype-gf-url}} */
6 | src: local('☺'),
7 | url(Exo+2_400_italic.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
8 | url(Exo+2_400_italic.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/G075hziEYGpfdK2KgVmqBQ.woff */
9 | url(Exo+2_400_italic.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/eq5mS-KPawZNVWb_JoPWQwLUuEpTyoUstqEm5AMlJo4.ttf */
10 | url(Exo+2_400_italic.svg#Exo+2_400_italic) format('svg'); /* {{svg-gf-url}} */
11 | }
12 | @font-face {
13 | font-family: 'Exo 2';
14 | font-style: normal;
15 | font-weight: 100;
16 | src: url(Exo+2_100_normal.eot); /* {{embedded-opentype-gf-url}} */
17 | src: local('☺'),
18 | url(Exo+2_100_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
19 | url(Exo+2_100_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/H184PiVPwxcA4lae41SXXA.woff */
20 | url(Exo+2_100_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/zsUPyQQR5Nf1c5_Wodx4EALUuEpTyoUstqEm5AMlJo4.ttf */
21 | url(Exo+2_100_normal.svg#Exo+2_100_normal) format('svg'); /* {{svg-gf-url}} */
22 | }
23 | @font-face {
24 | font-family: 'Exo 2';
25 | font-style: normal;
26 | font-weight: 300;
27 | src: url(Exo+2_300_normal.eot); /* {{embedded-opentype-gf-url}} */
28 | src: local('☺'),
29 | url(Exo+2_300_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
30 | url(Exo+2_300_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/JWvvdsUbb528VH-BDTzpW_esZW2xOQ-xsNqO47m55DA.woff */
31 | url(Exo+2_300_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/ngiFXK5ukde3w4E-Lmb_OnYhjbSpvc47ee6xR_80Hnw.ttf */
32 | url(Exo+2_300_normal.svg#Exo+2_300_normal) format('svg'); /* {{svg-gf-url}} */
33 | }
34 | @font-face {
35 | font-family: 'Exo 2';
36 | font-style: normal;
37 | font-weight: 400;
38 | src: url(Exo+2_400_normal.eot); /* {{embedded-opentype-gf-url}} */
39 | src: local('☺'),
40 | url(Exo+2_400_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
41 | url(Exo+2_400_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/8C2PVL2WIMUnPF90ukjrZQ.woff */
42 | url(Exo+2_400_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/K95WapF0Wa6u7CY0wsZbXqCWcynf_cDxXwCLxiixG1c.ttf */
43 | url(Exo+2_400_normal.svg#Exo+2_400_normal) format('svg'); /* http://fonts.gstatic.com/l/font?kit=UaJJNujq9sDPVrjoyxn8ng&skey=1b9a3dc5c6de9cce&v=v4#Exo2 */
44 | }
45 | @font-face {
46 | font-family: 'Exo 2';
47 | font-style: normal;
48 | font-weight: 500;
49 | src: url(Exo+2_500_normal.eot); /* {{embedded-opentype-gf-url}} */
50 | src: local('☺'),
51 | url(Exo+2_500_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
52 | url(Exo+2_500_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/SJSKlaAoPzG8E6EMHXZfevesZW2xOQ-xsNqO47m55DA.woff */
53 | url(Exo+2_500_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/Aj85fDXQrYnqAVDyNP57H3YhjbSpvc47ee6xR_80Hnw.ttf */
54 | url(Exo+2_500_normal.svg#Exo+2_500_normal) format('svg'); /* {{svg-gf-url}} */
55 | }
56 | @font-face {
57 | font-family: 'Exo 2';
58 | font-style: normal;
59 | font-weight: 700;
60 | src: url(Exo+2_700_normal.eot); /* {{embedded-opentype-gf-url}} */
61 | src: local('☺'),
62 | url(Exo+2_700_normal.eot?#iefix) format('embedded-opentype'), /* {{embedded-opentype-gf-url}} */
63 | url(Exo+2_700_normal.woff) format('woff'), /* http://fonts.gstatic.com/s/exo2/v4/RZBBdEhQV3g9mUXUAU9PpvesZW2xOQ-xsNqO47m55DA.woff */
64 | url(Exo+2_700_normal.ttf) format('truetype'), /* http://fonts.gstatic.com/s/exo2/v4/F-JaJbplW75-CW3MZ1qMbnYhjbSpvc47ee6xR_80Hnw.ttf */
65 | url(Exo+2_700_normal.svg#Exo+2_700_normal) format('svg'); /* {{svg-gf-url}} */
66 | }
67 |
--------------------------------------------------------------------------------
/common/models/test/server.test.js:
--------------------------------------------------------------------------------
1 | const loopback = require('loopback');
2 | const boot = require('loopback-boot');
3 | const axios = require('axios');
4 |
5 | const port = 3000;
6 | const host = 'localhost';
7 | const baseUrl = `http://${host}:${port}/api/Accounts`;
8 |
9 | beforeAll(() => {
10 | const app = loopback();
11 | app.start = function() {
12 | app.dataSource('db', {connector: 'memory'});
13 | const AccountSchema = require('../account.json');
14 | const Account = app.registry.createModel(AccountSchema);
15 | require('../account')(Account);
16 | app.model(Account, {dataSource: 'db'});
17 | app.use('/api', loopback.rest());
18 | return app.listen({host: host, port: port});
19 | };
20 |
21 | boot(app, __dirname, function(err) {
22 | if (err) throw err;
23 | app.start();
24 | });
25 | });
26 |
27 | describe('Sign up:', () => {
28 | beforeAll(async () => {
29 | await axios({
30 | url: `${baseUrl}`,
31 | method: 'get',
32 | }).catch((err) => err);
33 | });
34 | test('Should not pass no username and no password', async () => {
35 | return await expect(axios({
36 | url: `${baseUrl}`,
37 | method: 'post',
38 | data: {
39 | locale: 'en',
40 | },
41 | })).rejects.toThrow('Request failed with status code 422');
42 | });
43 |
44 | test('Should not pass no username', async () => {
45 | return await expect(axios({
46 | url: `${baseUrl}`,
47 | method: 'post',
48 | data: {
49 | password: 'password',
50 | },
51 | })).rejects.toThrow('Request failed with status code 422');
52 | });
53 |
54 | test('Should not pass less than 3 characters username', async () => {
55 | return await expect(axios({
56 | url: `${baseUrl}`,
57 | method: 'post',
58 | data: {
59 | username: 'si',
60 | password: 'password',
61 | },
62 | })).rejects.toThrow('Request failed with status code 422');
63 | });
64 |
65 | test('Should not pass no password', async () => {
66 | return await expect(axios({
67 | url: `${baseUrl}`,
68 | method: 'post',
69 | data: {
70 | username: 'test',
71 | },
72 | })).rejects.toThrow('Request failed with status code 422');
73 | });
74 |
75 | test('Should not pass less than 3 characters password', async () => {
76 | return await expect(axios({
77 | url: `${baseUrl}`,
78 | method: 'post',
79 | data: {
80 | username: 'test',
81 | password: 'pa',
82 | },
83 | })).rejects.toThrow('Request failed with status code 422');
84 | });
85 |
86 | test('Should pass correct username and password', async () => {
87 | return await expect(axios({
88 | url: `${baseUrl}`,
89 | method: 'post',
90 | data: {
91 | locale: 'en',
92 | username: 'test',
93 | password: 'password',
94 | },
95 | })).resolves.toEqual(expect.objectContaining({status: 200}));
96 | });
97 |
98 | test('Should not pass existing username', async () => {
99 | return await expect(axios({
100 | url: `${baseUrl}`,
101 | method: 'post',
102 | data: {
103 | username: 'test',
104 | password: 'password',
105 | },
106 | })).rejects.toThrow('Request failed with status code 422');
107 | });
108 | });
109 |
110 | describe('Log in:', () => {
111 | test('Should not pass no username and no password', async () => {
112 | return await expect(axios({
113 | url: `${baseUrl}/login`,
114 | method: 'post',
115 | data: {
116 | },
117 | })).rejects.toThrow('Request failed with status code 400');
118 | });
119 |
120 | test('Should not pass no username', async () => {
121 | return await expect(axios({
122 | url: `${baseUrl}/login`,
123 | method: 'post',
124 | data: {
125 | password: 'password',
126 | },
127 | })).rejects.toThrow('Request failed with status code 400');
128 | });
129 |
130 | test('Should not pass no password', async () => {
131 | return await expect(axios({
132 | url: `${baseUrl}/login`,
133 | method: 'post',
134 | data: {
135 | username: 'test',
136 | },
137 | })).rejects.toThrow('Request failed with status code 401');
138 | });
139 |
140 | test('Should not pass invalid username', async () => {
141 | return await expect(axios({
142 | url: `${baseUrl}/login`,
143 | method: 'post',
144 | data: {
145 | username: 'invalid',
146 | password: 'password',
147 | },
148 | })).rejects.toThrow('Request failed with status code 401');
149 | });
150 | });
151 |
--------------------------------------------------------------------------------
/common/models/account.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Account",
3 | "base": "User",
4 | "excludeBaseProperties": [
5 | "email", "emailVerified", "realm", "verificationToken"
6 | ],
7 | "hidden": [
8 | "seCounter",
9 | "seSecretAscii",
10 | "seSecretHex",
11 | "seSecretBase32",
12 | "seSecretUrl"
13 | ],
14 | "idInjection": true,
15 | "options": {
16 | "validateUpsert": true
17 | },
18 | "properties": {
19 | "adamantAddress": {
20 | "default": null,
21 | "required": false,
22 | "type": "string"
23 | },
24 | "locale": {
25 | "default": "en",
26 | "required": true,
27 | "type": "string"
28 | },
29 | "password": {
30 | "required": true,
31 | "type": "string"
32 | },
33 | "se2faEnabled": {
34 | "type": "boolean"
35 | },
36 | "seCounter": {
37 | "type": "number"
38 | },
39 | "seSecretAscii": {
40 | "type": "string"
41 | },
42 | "seSecretHex": {
43 | "type": "string"
44 | },
45 | "seSecretBase32": {
46 | "type": "string"
47 | },
48 | "seSecretUrl": {
49 | "type": "string"
50 | },
51 | "username": {
52 | "required": true,
53 | "type": "string"
54 | }
55 | },
56 | "validations": [],
57 | "relations": {},
58 | "acls": [
59 | {
60 | "accessType": "*",
61 | "principalType": "ROLE",
62 | "principalId": "$everyone",
63 | "permission": "DENY"
64 | },
65 | {
66 | "accessType": "EXECUTE",
67 | "principalType": "ROLE",
68 | "principalId": "$owner",
69 | "permission": "ALLOW",
70 | "property": "verify2fa"
71 | },
72 | {
73 | "accessType": "EXECUTE",
74 | "principalType": "ROLE",
75 | "principalId": "authorized",
76 | "permission": "ALLOW",
77 | "property": [
78 | "disable2fa", "enable2fa", "updateAdamantAddress", "updateLocale"
79 | ]
80 | }
81 | ],
82 | "methods": {
83 | "prototype.disable2fa": {
84 | "returns": [
85 | {
86 | "arg": "data",
87 | "description": "",
88 | "root": true,
89 | "type": "Account"
90 | }
91 | ],
92 | "description": "Disable 2FA for account",
93 | "http": [
94 | {
95 | "path": "/disable2fa",
96 | "verb": "get"
97 | }
98 | ]
99 | },
100 | "prototype.enable2fa": {
101 | "accepts": [
102 | {
103 | "arg": "hotp",
104 | "description": "",
105 | "required": true,
106 | "type": "string"
107 | }
108 | ],
109 | "returns": [
110 | {
111 | "arg": "data",
112 | "description": "",
113 | "root": true,
114 | "type": "Account"
115 | }
116 | ],
117 | "description": "Verify HOTP and enable 2FA",
118 | "http": [
119 | {
120 | "path": "/enable2fa",
121 | "verb": "get"
122 | }
123 | ]
124 | },
125 | "prototype.updateLocale": {
126 | "accepts": [
127 | {
128 | "arg": "locale",
129 | "description": "",
130 | "required": true,
131 | "type": "string"
132 | }
133 | ],
134 | "description": "Update account's i18n locale",
135 | "returns": [
136 | {
137 | "arg": "data",
138 | "description": "",
139 | "root": true,
140 | "type": "Account"
141 | }
142 | ],
143 | "http": [
144 | {
145 | "path": "/locale",
146 | "verb": "post"
147 | }
148 | ]
149 | },
150 | "prototype.updateAdamantAddress": {
151 | "accepts": [
152 | {
153 | "arg": "adamantAddress",
154 | "description": "",
155 | "required": true,
156 | "type": "string"
157 | }
158 | ],
159 | "description": "Update account's ADAMANT address and regenerate secret key",
160 | "http": [
161 | {
162 | "path": "/adamantAddress",
163 | "verb": "post"
164 | }
165 | ],
166 | "returns": [
167 | {
168 | "arg": "data",
169 | "description": "",
170 | "root": true,
171 | "type": "Account"
172 | }
173 | ]
174 | },
175 | "prototype.verify2fa": {
176 | "accepts": [
177 | {
178 | "arg": "hotp",
179 | "description": "",
180 | "required": true,
181 | "type": "string"
182 | }
183 | ],
184 | "description": "Verify HOTP and authorize to access settings",
185 | "http": [
186 | {
187 | "path": "/verify2fa",
188 | "verb": "post"
189 | }
190 | ],
191 | "returns": [
192 | {
193 | "arg": "data",
194 | "description": "",
195 | "root": true,
196 | "type": "object"
197 | }
198 | ]
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/client/public/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
84 |
--------------------------------------------------------------------------------
/helpers/test/logger.test.js:
--------------------------------------------------------------------------------
1 | const logger = require('../logger');
2 | const constants = require('./constants');
3 |
4 | describe('logger: log', () => {
5 | const logLevel = 'log';
6 |
7 | test('Should log log level', (done) => {
8 | logger.initLogger(logLevel, {
9 | log(...objs) {
10 | const str = objs.join('');
11 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.LOG_FORMAT_REGEX.source + /(log)/.source)));
12 |
13 | done();
14 | },
15 | });
16 |
17 | logger.log('log');
18 | });
19 |
20 | test('Should log info level', (done) => {
21 | logger.initLogger(logLevel, {
22 | log(...objs) {
23 | const str = objs.join('');
24 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.INFO_FORMAT_REGEX.source + /(info)/.source)));
25 |
26 | done();
27 | },
28 | });
29 |
30 | logger.info('info');
31 | });
32 |
33 | test('Should log warn level', (done) => {
34 | logger.initLogger(logLevel, {
35 | log(...objs) {
36 | const str = objs.join('');
37 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.WARN_FORMAT_REGEX.source + /(warn)/.source)));
38 |
39 | done();
40 | },
41 | });
42 |
43 | logger.warn('warn');
44 | });
45 |
46 | test('Should log error level', (done) => {
47 | logger.initLogger(logLevel, {
48 | log(...objs) {
49 | const str = objs.join('');
50 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.ERROR_FORMAT_REGEX.source + /(error)/.source)));
51 |
52 | done();
53 | },
54 | });
55 |
56 | logger.error('error');
57 | });
58 | });
59 |
60 | describe('logger: info', () => {
61 | const logLevel = 'info';
62 |
63 | test('Should not log log level', (done) => {
64 | logger.initLogger(logLevel, {
65 | log() {
66 | done('Log level has been called');
67 | },
68 | });
69 |
70 | logger.log('log');
71 | done();
72 | });
73 |
74 | test('Should log info level', (done) => {
75 | logger.initLogger(logLevel, {
76 | log(...objs) {
77 | const str = objs.join('');
78 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.INFO_FORMAT_REGEX.source + /(info)/.source)));
79 |
80 | done();
81 | },
82 | });
83 |
84 | logger.info('info');
85 | });
86 |
87 | test('Should log warn level', (done) => {
88 | logger.initLogger(logLevel, {
89 | log(...objs) {
90 | const str = objs.join('');
91 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.WARN_FORMAT_REGEX.source + /(warn)/.source)));
92 |
93 | done();
94 | },
95 | });
96 |
97 | logger.warn('warn');
98 | });
99 |
100 | test('Should log error level', (done) => {
101 | logger.initLogger(logLevel, {
102 | log(...objs) {
103 | const str = objs.join('');
104 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.ERROR_FORMAT_REGEX.source + /(error)/.source)));
105 |
106 | done();
107 | },
108 | });
109 |
110 | logger.error('error');
111 | });
112 | });
113 |
114 | describe('logger: warn', () => {
115 | const logLevel = 'warn';
116 |
117 | test('Should not log log level', (done) => {
118 | logger.initLogger(logLevel, {
119 | log() {
120 | done('Log level has been called');
121 | },
122 | });
123 |
124 | logger.log('log');
125 | done();
126 | });
127 |
128 | test('Should not log info level', (done) => {
129 | logger.initLogger(logLevel, {
130 | log() {
131 | done('Info level has been called');
132 | },
133 | });
134 |
135 | logger.info('info');
136 | done();
137 | });
138 |
139 | test('Should log warn level', (done) => {
140 | logger.initLogger(logLevel, {
141 | log(...objs) {
142 | const str = objs.join('');
143 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.WARN_FORMAT_REGEX.source + /(warn)/.source)));
144 |
145 | done();
146 | },
147 | });
148 |
149 | logger.warn('warn');
150 | });
151 |
152 | test('Should log error level', (done) => {
153 | logger.initLogger(logLevel, {
154 | log(...objs) {
155 | const str = objs.join('');
156 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.ERROR_FORMAT_REGEX.source + /(error)/.source)));
157 |
158 | done();
159 | },
160 | });
161 |
162 | logger.error('error');
163 | });
164 | });
165 |
166 | describe('logger: error', () => {
167 | const logLevel = 'error';
168 |
169 | test('Should not log log level', (done) => {
170 | logger.initLogger(logLevel, {
171 | log() {
172 | done('Log level has been called');
173 | },
174 | });
175 |
176 | logger.log('log');
177 | done();
178 | });
179 |
180 | test('Should not log info level', (done) => {
181 | logger.initLogger(logLevel, {
182 | log() {
183 | done('Info level has been called');
184 | },
185 | });
186 |
187 | logger.info('info');
188 | done();
189 | });
190 |
191 | test('Should not log warn level', (done) => {
192 | logger.initLogger(logLevel, {
193 | log() {
194 | done('Warn level has been called');
195 | },
196 | });
197 |
198 | logger.warn('warn');
199 | done();
200 | });
201 |
202 | test('Should log error level', (done) => {
203 | logger.initLogger(logLevel, {
204 | log(...objs) {
205 | const str = objs.join('');
206 | expect(str).toEqual(expect.stringMatching(new RegExp(constants.ERROR_FORMAT_REGEX.source + /(error)/.source)));
207 |
208 | done();
209 | },
210 | });
211 |
212 | logger.error('error');
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/client/src/views/Verify.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
16 |
21 |
25 |
29 |
30 |
35 |
36 |
43 |
47 |
48 |
61 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
167 |
168 |
227 |
--------------------------------------------------------------------------------
/client/src/views/Signup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
16 |
21 |
25 |
29 |
33 |
34 |
39 |
40 |
47 |
48 |
62 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
194 |
195 |
253 |
--------------------------------------------------------------------------------
/client/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
16 |
21 |
25 |
29 |
33 |
34 |
39 |
40 |
47 |
48 |
62 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
95 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
214 |
215 |
275 |
--------------------------------------------------------------------------------
/client/src/store.js:
--------------------------------------------------------------------------------
1 | import createPersistedState from 'vuex-persistedstate'
2 | import Vue from 'vue'
3 | import Vuex from 'vuex'
4 |
5 | Vue.use(Vuex)
6 |
7 | export default new Vuex.Store({
8 | actions: {
9 | disable2fa ({ commit, state }) {
10 | return Vue.axios.get(`${state.apiUrl}${state.account.id}/disable2fa`, {
11 | params: {
12 | access_token: state.session.id,
13 | id: state.account.id
14 | }
15 | }).then(res => {
16 | if (res.status === 200) {
17 | commit('updateAccount', {
18 | se2faEnabled: res.data.se2faEnabled
19 | })
20 | console.info('2FA auth disabled:', res)
21 | } else console.warn('Failed to disable 2FA auth:', res)
22 | return res.status
23 | }).catch(error => {
24 | console.error('Error while disabling 2FA auth:', error)
25 | return error.response.status
26 | })
27 | },
28 | enable2fa ({ commit, state }, hotp) {
29 | return Vue.axios.get(`${state.apiUrl}${state.account.id}/enable2fa`, {
30 | params: {
31 | access_token: state.session.id,
32 | id: state.account.id,
33 | hotp
34 | }
35 | }).then(res => {
36 | if (res.status === 200) {
37 | commit('updateAccount', {
38 | se2faEnabled: res.data.se2faEnabled
39 | })
40 | commit('updateSession', {
41 | // If 2FA enabled, verification already passed and vice versa
42 | se2faVerified: res.data.se2faEnabled
43 | })
44 | console.info('2FA auth enabled:', res)
45 | } else console.warn('Failed to enable 2FA auth:', res)
46 | return res.status
47 | }).catch(error => {
48 | console.error('Error while enabling 2FA auth:', error)
49 | return error.response
50 | })
51 | },
52 | login ({ commit, state }, params) {
53 | return Vue.axios.post(state.apiUrl + 'login', params).then(res => {
54 | if (res.status === 200) {
55 | commit('setSession', {
56 | created: res.data.created,
57 | id: res.data.id,
58 | lastSeen: Date.now(),
59 | timeDelta: Date.now() - Date.parse(res.data.created),
60 | ttl: res.data.ttl,
61 | se2faVerified: res.data.se2faEnabled ? null : true
62 | })
63 | commit('setAccount', {
64 | adamantAddress: res.data.adamantAddress,
65 | id: res.data.userId,
66 | locale: res.data.locale,
67 | se2faEnabled: res.data.se2faEnabled,
68 | username: res.data.username
69 | })
70 | console.info('Login successful:', res)
71 | } else console.warn('Login failed:', res)
72 | return res
73 | }).catch(error => {
74 | console.error('Error occurred while Login:', error)
75 | return error.response || { status: 503 }
76 | })
77 | },
78 | logout ({ commit, state }) {
79 | return Vue.axios.post(
80 | `${state.apiUrl}logout/?access_token=${state.session.id}`
81 | ).then(res => {
82 | if (res.status === 204) {
83 | commit('clearSession')
84 | console.info('Logout successful:', res)
85 | } else console.warn('Logout failed:', res)
86 | return res.status
87 | }).catch(error => {
88 | console.error('Error occurred while Logout:', error)
89 | // Clear expired session even if backend is not available
90 | commit('clearSession')
91 | return error.response ? error.response.status : 503
92 | })
93 | },
94 | postAdamantAddress ({ commit, state }, adamantAddress) {
95 | return Vue.axios.post(
96 | `${state.apiUrl}${state.account.id}/adamantAddress?access_token=${state.session.id}`,
97 | {
98 | adamantAddress
99 | }
100 | ).then(res => {
101 | if (res.status === 200) {
102 | commit('updateAccount', {
103 | adamantAddress: res.data.adamantAddress
104 | })
105 | console.info('ADAMANT address submitted:', res)
106 | } else console.warn('Failed to submit ADAMANT address:', res)
107 | return res
108 | }).catch(error => {
109 | console.error('Error occurred while submitting ADAMANT address:', error)
110 | return error.response
111 | })
112 | },
113 | postLocale ({ commit, state }, locale) {
114 | return Vue.axios.post(
115 | `${state.apiUrl}${state.account.id}/locale?access_token=${state.session.id}`,
116 | {
117 | id: state.account.id,
118 | locale
119 | }
120 | ).then(res => {
121 | if (res.status === 200) {
122 | commit('updateAccount', {
123 | locale: res.data.locale
124 | })
125 | console.info('Locale submitted:', res)
126 | } else console.warn('Failed to submit locale:', res)
127 | return res.status
128 | }).catch(error => {
129 | console.error('Error occurred while submitting locale:', error)
130 | return error.response.status
131 | })
132 | },
133 | signup ({ state }, params) {
134 | return Vue.axios.post(state.apiUrl, params).then(res => {
135 | if (res.status === 200) {
136 | console.info('Signup successful:', res)
137 | } else console.warn('Signup failed:', res)
138 | return res.status
139 | }).catch(error => {
140 | console.error('Error occurred while Signup:', error)
141 | return error.response ? error.response.status : 503
142 | })
143 | },
144 | verify2fa ({ commit, state }, hotp) {
145 | return Vue.axios.post(
146 | `${state.apiUrl}${state.account.id}/verify2fa?access_token=${state.session.id}`,
147 | {
148 | id: state.account.id,
149 | hotp
150 | }
151 | ).then(res => {
152 | if (res.status === 200) {
153 | commit('updateSession', {
154 | se2faVerified: res.data.se2faVerified
155 | })
156 | console.info('2FA code verified:', res)
157 | } else console.warn('Failed to verify 2FA code:', res)
158 | return res.status
159 | }).catch(error => {
160 | console.error('Error occurred while verifying 2FA code:', error)
161 | return error.response
162 | })
163 | }
164 | },
165 | getters: {
166 | sessionTimeLeft: state => {
167 | return (
168 | Date.parse(state.session.created) +
169 | state.session.timeDelta +
170 | state.session.ttl
171 | ) - Date.now()
172 | }
173 | },
174 | mutations: {
175 | clearSession: state => {
176 | // Shallow iteration
177 | for (const k in state.session) {
178 | if (Object.prototype.hasOwnProperty.call(state.session, k)) {
179 | state.session[k] = null
180 | }
181 | }
182 | state.session.lastSeen = Date.now()
183 | },
184 | setAccount: (state, account) => {
185 | state.account = account
186 | },
187 | setSession: (state, session) => {
188 | state.session = session
189 | },
190 | updateAccount: (state, account) => {
191 | Object.assign(state.account, account)
192 | },
193 | updateSession: (state, session) => {
194 | Object.assign(state.session, session)
195 | }
196 | },
197 | plugins: [
198 | createPersistedState({ storage: window.localStorage })
199 | ],
200 | state: {
201 | account: {
202 | adamantAddress: null,
203 | id: null,
204 | locale: null,
205 | se2faEnabled: null,
206 | username: null
207 | },
208 | apiUrl: getApiUrl(),
209 | session: {
210 | created: null, // Created ISO timestamp string
211 | id: null, // Access token
212 | lastSeen: null, // Indicates that user had been logged at least once
213 | timeDelta: null, // Difference between server and client time
214 | ttl: null, // Time to live, 20 minutes 16 seconds approximately by default
215 | se2faVerified: null // Indicates that user logged in and passed 2FA
216 | }
217 | },
218 | strict: process.env.NODE_ENV !== 'production'
219 | })
220 |
221 | function getApiUrl () {
222 | if (process.env.NODE_ENV === 'production') {
223 | return window.location.host.includes('onion')
224 | ? `http://${window.location.host}/api/Accounts/`
225 | : `https://${window.location.host}/api/Accounts/`
226 | } else {
227 | return 'http://localhost:3000/api/Accounts/'
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/client/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
16 |
20 |
21 |
27 |
28 |
32 |
33 |
34 |
35 |
39 |
40 |
46 |
53 |
59 |
60 |
68 |
82 |
83 |
88 |
93 |
99 |
100 |
101 |
102 |
116 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
295 |
296 |
326 |
--------------------------------------------------------------------------------
/common/models/account.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const g = require('loopback/lib/globalize');
4 | const speakeasy = require('speakeasy');
5 | const logger = require('../../helpers/logger');
6 | const adamantApi = require('adamant-console');
7 | const MAX_PASSWORD_LENGTH = 15;
8 | const MIN_PASSWORD_LENGTH = 3;
9 |
10 | module.exports = function(Account) {
11 | Account.prototype.enable2fa = function(hotp, next) {
12 | let se2faEnabled = false;
13 | if (/^\d{6}$/.test(hotp)) {
14 | se2faEnabled = speakeasy.hotp.verify({
15 | counter: this.seCounter,
16 | // encoding: 'ascii',
17 | secret: this.seSecretAscii,
18 | token: hotp,
19 | });
20 | if (se2faEnabled) {
21 | this.updateAttributes({
22 | se2faEnabled: true,
23 | seCounter: this.seCounter,
24 | }, (error) => {
25 | if (error) return next(error);
26 | const Role = Account.app.models.Role;
27 | const RoleMapping = Account.app.models.RoleMapping;
28 | // 2FA enabled and verified, allow
29 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
30 | if (error) return next(error);
31 | RoleMapping.findOrCreate({where: {principalId: this.id}}, {
32 | principalType: 'USER',
33 | principalId: this.id,
34 | roleId: role.getId(),
35 | }, (error) => {
36 | if (error) return next(error);
37 | next(null, {se2faEnabled});
38 | });
39 | });
40 | });
41 | } else next(null, {se2faEnabled});
42 | } else next(null, {se2faEnabled});
43 | };
44 |
45 | Account.prototype.disable2fa = function(next) {
46 | const res = {se2faEnabled: false};
47 | this.updateAttributes(res, (error) => {
48 | if (error) return next(error);
49 | next(null, res);
50 | });
51 | };
52 |
53 | Account.prototype.updateLocale = function(locale, next) {
54 | const res = {locale};
55 | this.updateAttributes(res, (error) => {
56 | if (error) return next(error);
57 | next(null, res);
58 | });
59 | };
60 |
61 | Account.prototype.updateAdamantAddress = function(adamantAddress, next) {
62 | const secret = speakeasy.generateSecret({
63 | name: 'ADAMANT-' + adamantAddress,
64 | });
65 | const data = {
66 | // adamantAddress,
67 | seCounter: 0,
68 | seSecretAscii: secret.ascii,
69 | seSecretBase32: secret.base32,
70 | seSecretHex: secret.hex,
71 | seSecretUrl: secret.otpauth_url,
72 | };
73 | this.updateAttributes(data, (error) => {
74 | if (error) return next(error);
75 | send2fa(adamantAddress, this).then((result) => {
76 | if (result.success) {
77 | this.updateAttribute('adamantAddress', adamantAddress, (error) => {
78 | if (error) return next(error);
79 | next(null, {...result, ...{adamantAddress}});
80 | });
81 | } else {
82 | const error = new Error(g.f('Unable to send 2FA code'));
83 | error.statusCode = 900;
84 | error.code = result?.errorCode;
85 | /**
86 | * { error.
87 | * code: "WRONG_PASSPHRASE"
88 | * message: "Unable to send 2FA code"
89 | * name: "Error"
90 | * stack: "Error: Unable to send 2FA code\n at /Users/..account.js:85:25.."
91 | * statusCode: 500
92 | * }
93 | */
94 | next(error);
95 | }
96 | });
97 | });
98 | };
99 |
100 | Account.prototype.verify2fa = function(hotp, next) {
101 | let se2faVerified = false;
102 | if (/^\d{6}$/.test(hotp)) {
103 | se2faVerified = speakeasy.hotp.verify({
104 | counter: this.seCounter,
105 | // encoding: 'ascii',
106 | secret: this.seSecretAscii,
107 | token: hotp,
108 | });
109 | if (se2faVerified) {
110 | this.updateAttribute('seCounter', this.seCounter, (error) => {
111 | if (error) return next(error);
112 | const Role = Account.app.models.Role;
113 | const RoleMapping = Account.app.models.RoleMapping;
114 | // 2FA verification passed, allow
115 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
116 | if (error) return next(error);
117 | RoleMapping.findOrCreate({where: {principalId: this.id}}, {
118 | principalType: 'USER',
119 | principalId: this.id,
120 | roleId: role.getId(),
121 | }, (error) => {
122 | if (error) return next(error);
123 | next(null, {se2faVerified});
124 | });
125 | });
126 | });
127 | } else next(null, {se2faVerified});
128 | } else next(null, {se2faVerified});
129 | };
130 |
131 | // Failed request goes to afterRemoteError handler instead
132 | Account.afterRemote('login', function(ctx, res, next) {
133 | Account.findById(res.userId, (error, account) => {
134 | if (error) return next(error);
135 | const Role = Account.app.models.Role;
136 | const RoleMapping = Account.app.models.RoleMapping;
137 | res.setAttributes({
138 | adamantAddress: account.adamantAddress,
139 | locale: account.locale,
140 | se2faEnabled: account.se2faEnabled,
141 | username: account.username,
142 | });
143 | // Check that user authorized to access account
144 | if (account.se2faEnabled) {
145 | // 2FA enabled
146 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
147 | if (error) return next(error);
148 | RoleMapping.findOne({where: {principalId: account.id}}, {
149 | principalType: 'USER',
150 | principalId: account.id,
151 | roleId: role.getId(),
152 | }, (error, roleMapping) => {
153 | if (error) return next(error);
154 | if (roleMapping) {
155 | // Revoke previously assigned role and wait for 2FA verification
156 | roleMapping.destroy((error) => {
157 | if (error) return next(error);
158 | send2fa(res.adamantAddress, account).then((result) => {
159 | if (result?.success) {
160 | res.setAttribute('se2faTx', (result).transactionId);
161 | next(null, res);
162 | } else {
163 | const error = new Error(g.f('Unable to send 2FA code'));
164 | error.statusCode = 900;
165 | error.code = result?.errorCode;
166 | next(error);
167 | }
168 | });
169 | });
170 | } else {
171 | send2fa(res.adamantAddress, account).then((result) => {
172 | if (result?.success) {
173 | res.setAttribute('se2faTx', result.transactionId);
174 | next(null, res);
175 | } else {
176 | const error = new Error(g.f('Unable to send 2FA code'));
177 | error.statusCode = 900;
178 | error.code = result?.errorCode;
179 | next(error);
180 | }
181 | });
182 | }
183 | });
184 | });
185 | } else {
186 | // 2FA disabled, allow
187 | Role.findOne({where: {name: 'authorized'}}, (error, role) => {
188 | if (error) return next(error);
189 | RoleMapping.findOrCreate({where: {principalId: account.id}}, {
190 | principalType: 'USER',
191 | principalId: account.id,
192 | roleId: role.getId(),
193 | }, (error) => {
194 | if (error) return next(error);
195 | next(null, res);
196 | });
197 | });
198 | }
199 | });
200 | });
201 |
202 | Account.afterRemote('prototype.updateAdamantAddress', function(ctx, res, next) {
203 | let error;
204 | if (res.error) {
205 | error = new Error();
206 | error.statusCode = 422;
207 | error.message = error.message || res.error.toLowerCase();
208 | error.code = res.error.toUpperCase().replace(' ', '_');
209 | }
210 | next(error);
211 | });
212 |
213 | Account.validatesExclusionOf('username', {
214 | in: ['admin'],
215 | message: 'This username not allowed',
216 | });
217 | Account.validatesFormatOf('adamantAddress', {
218 | allowNull: true,
219 | message: 'Address does not match pattern',
220 | with: /^U\d+$/,
221 | });
222 | Account.validatesFormatOf('locale', {
223 | allowNull: true,
224 | message: 'Locale does not match pattern',
225 | with: /^[a-z]{2}$/,
226 | });
227 | Account.validatesLengthOf('adamantAddress', {
228 | allowNull: true,
229 | message: {
230 | max: 'Address is too long',
231 | min: 'Address is too short',
232 | },
233 | max: 23,
234 | min: 7,
235 | });
236 | Account.validatesLengthOf('username', {
237 | message: {
238 | max: 'Username is too long',
239 | min: 'Username is too short',
240 | },
241 | max: 25,
242 | min: 3,
243 | });
244 | Account.validatesPresenceOf('username', 'password');
245 | Account.validatesUniquenessOf('username', {
246 | message: 'User already exists',
247 | });
248 |
249 | // validatesLengthOf is not applicable for password.
250 | // Recommended solution is to override User.validatePassword method:
251 | // https://github.com/strongloop/loopback/pull/941
252 | Account.validatePassword = function(plain) {
253 | let error;
254 | if (!plain || typeof plain !== 'string') {
255 | error = new Error(g.f('Invalid password.'));
256 | error.code = 'INVALID_PASSWORD';
257 | error.statusCode = 422;
258 | throw error;
259 | }
260 | // Bcrypt only supports up to 72 bytes; the rest is silently dropped.
261 | const len = Buffer.byteLength(plain, 'utf8');
262 | if (len > MAX_PASSWORD_LENGTH) {
263 | error = new Error(g.f('The password entered was too long. Max length is %d (entered %d)',
264 | MAX_PASSWORD_LENGTH, len));
265 | error.code = 'PASSWORD_TOO_LONG';
266 | error.statusCode = 422;
267 | throw error;
268 | }
269 | if (len < MIN_PASSWORD_LENGTH) {
270 | error = new Error(g.f('The password entered was too short. Min length is %d (entered %d)',
271 | MIN_PASSWORD_LENGTH, len));
272 | error.code = 'PASSWORD_TOO_SHORT';
273 | error.statusCode = 422;
274 | throw error;
275 | }
276 | };
277 |
278 | function send2fa(adamantAddress, account) {
279 | const counter = account.seCounter + 1;
280 | return new Promise((resolve, reject) => {
281 | account.updateAttribute('seCounter', counter, async (err) => {
282 | if (err) return reject(err);
283 | const hotp = speakeasy.hotp({
284 | counter,
285 | // encoding: 'ascii',
286 | secret: account.seSecretAscii,
287 | });
288 |
289 | const message = `2FA code: ${hotp}`;
290 | adamantApi.sendMessage(
291 | adamantAddress,
292 | message,
293 | ).then((res) => {
294 | if (res?.success) {
295 | logger.log(`2FA message '${message}' sent to ${adamantAddress}: ${JSON.stringify(res)}`);
296 | resolve(res);
297 | } else {
298 | res = res || {};
299 | logger.error(`Failed to send ADM message '${message}' to ${adamantAddress}. ${res?.errorMessage}.`);
300 | if (res?.errorMessage?.includes('Mnemonic')) {
301 | res.errorCode = 'WRONG_PASSPHRASE';
302 | } else if (res?.errorMessage?.includes('not have enough ADM')) {
303 | res.errorCode = 'NOT_ENOUGH_ADM';
304 | } else if (res?.errorMessage?.includes('uninitialized')) {
305 | res.errorCode = 'RECIPIENT_UNINITIALIZED';
306 | } else {
307 | res.errorCode = 'NOT_SENT_GENERAL';
308 | }
309 | resolve(res);
310 | }
311 | }).catch((err) => {
312 | logger.error(`Error while sending ADM message '${message}' to ${adamantAddress}. ${err}.`);
313 | resolve(err);
314 | });
315 | });
316 | }).catch((err) => logger.error(err));
317 | }
318 | };
319 |
--------------------------------------------------------------------------------
/client/public/fonts/Roboto_400_normal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
309 |
--------------------------------------------------------------------------------