├── .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 | 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 | 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 | 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 | 14 | 15 | 56 | 57 | 68 | -------------------------------------------------------------------------------- /client/src/components/NavigationMenu.vue: -------------------------------------------------------------------------------- 1 | 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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 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 | 75 | 76 | 167 | 168 | 227 | -------------------------------------------------------------------------------- /client/src/views/Signup.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 194 | 195 | 253 | -------------------------------------------------------------------------------- /client/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 39 | 40 | 42 | 44 | 45 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 59 | 60 | 62 | 64 | 65 | 66 | 67 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 78 | 79 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 102 | 103 | 105 | 106 | 108 | 109 | 110 | 111 | 112 | 113 | 115 | 116 | 118 | 120 | 122 | 123 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 140 | 142 | 144 | 145 | 146 | 149 | 150 | 153 | 155 | 156 | 157 | 158 | 161 | 162 | 164 | 165 | 166 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 176 | 177 | 178 | 180 | 183 | 185 | 186 | 187 | 188 | 190 | 192 | 194 | 195 | 197 | 198 | 199 | 200 | 202 | 203 | 204 | 205 | 206 | 207 | 209 | 211 | 213 | 215 | 218 | 221 | 222 | 224 | 225 | 226 | 228 | 230 | 231 | 232 | 234 | 236 | 238 | 240 | 243 | 246 | 249 | 252 | 254 | 256 | 258 | 260 | 262 | 263 | 264 | 265 | 266 | 268 | 270 | 272 | 274 | 276 | 279 | 281 | 283 | 285 | 286 | 287 | 288 | 290 | 291 | 293 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | --------------------------------------------------------------------------------