├── src
├── lib
│ ├── index.js
│ ├── browser.js
│ ├── validators.js
│ └── utils.js
├── components
│ ├── Card
│ │ ├── index.js
│ │ ├── Image.jsx
│ │ ├── Card.module.scss
│ │ └── Card.jsx
│ ├── Modal
│ │ ├── index.js
│ │ ├── Modal.module.scss
│ │ └── Modal.jsx
│ ├── Avatar
│ │ ├── index.js
│ │ ├── Avatar.module.scss
│ │ └── Avatar.jsx
│ ├── Button
│ │ ├── index.js
│ │ ├── Button.jsx
│ │ └── Button.module.scss
│ ├── Dropdown
│ │ ├── index.js
│ │ ├── Dropdown.module.scss
│ │ └── Dropdown.jsx
│ ├── Loader
│ │ ├── index.js
│ │ ├── Loader.jsx
│ │ └── Loader.module.scss
│ ├── Switch
│ │ ├── index.js
│ │ ├── SwitchButton.jsx
│ │ ├── Switch.jsx
│ │ └── Switch.module.scss
│ ├── TextField
│ │ ├── index.js
│ │ ├── TextField.module.scss
│ │ └── TextField.jsx
│ ├── Typography
│ │ ├── index.js
│ │ ├── Typography.jsx
│ │ └── Typography.module.scss
│ ├── Notification
│ │ ├── index.js
│ │ ├── Notification.module.scss
│ │ ├── NotificationBase.module.scss
│ │ ├── NotificationBase.jsx
│ │ └── Notification.jsx
│ ├── CircleIndicator
│ │ ├── index.js
│ │ ├── CircleIndicator.module.scss
│ │ └── CircleIndicator.jsx
│ ├── PasswordStrength
│ │ ├── index.js
│ │ ├── PasswordStrength.module.scss
│ │ └── PasswordStrength.jsx
│ ├── Space.jsx
│ └── index.js
├── containers
│ ├── Login
│ │ ├── index.js
│ │ └── Login.module.scss
│ ├── Devices
│ │ ├── index.js
│ │ ├── Devices.module.scss
│ │ └── Devices.jsx
│ ├── Landing
│ │ ├── index.js
│ │ ├── Landing.module.scss
│ │ └── Landing.jsx
│ ├── Loading
│ │ ├── index.js
│ │ ├── Loading.module.scss
│ │ └── Loading.jsx
│ ├── Navbar
│ │ ├── index.js
│ │ ├── Navbar.jsx
│ │ ├── NavbarMin.module.scss
│ │ ├── NavbarMax.module.scss
│ │ ├── NavbarMobile.module.scss
│ │ ├── NavbarMin.jsx
│ │ ├── NavbarMax.jsx
│ │ └── NavbarMobile.jsx
│ ├── Profile
│ │ ├── index.js
│ │ └── Profile.module.scss
│ ├── Applications
│ │ ├── index.js
│ │ ├── Link.jsx
│ │ ├── Applications.module.scss
│ │ └── Applications.jsx
│ └── index.js
├── styles
│ ├── variables.scss
│ ├── mixins.scss
│ ├── index.scss
│ └── colors.scss
├── assets
│ ├── masq_loading.gif
│ ├── bg-landing.svg
│ ├── check-circle.svg
│ ├── plus-square.svg
│ ├── background.svg
│ ├── back-to-maps.svg
│ ├── cubes-sidebar-min.svg
│ ├── windows-masq.svg
│ ├── cubes-sidebar.svg
│ ├── shield.svg
│ ├── cubes.svg
│ ├── head-landing-mobile.svg
│ ├── head-landing.svg
│ ├── logo-sidebar.svg
│ ├── logo.svg
│ ├── qwant.svg
│ ├── hard-disk.svg
│ └── apps-maps.svg
├── hooks
│ ├── index.js
│ ├── useWindowWidth.js
│ └── useWindowHeight.js
├── App.module.scss
├── reducers
│ ├── index.js
│ ├── loading.js
│ ├── notification.js
│ └── masq.js
├── modals
│ ├── PersistentStorageRequest
│ │ ├── PersistentStorageRequest.module.scss
│ │ └── PersistentStorageRequest.jsx
│ ├── UnsupportedBrowser
│ │ ├── UnsupportedBrowser.module.scss
│ │ └── UnsupportedBrowser.jsx
│ ├── PasswordEdit
│ │ ├── PasswordEdit.module.scss
│ │ └── PasswordEdit.jsx
│ ├── DeleteAppDialog
│ │ ├── DeleteAppDialog.module.scss
│ │ └── DeleteAppDialog.jsx
│ ├── DeleteProfileDialog
│ │ ├── DeleteProfileDialog.module.scss
│ │ └── DeleteProfileDialog.jsx
│ ├── SyncDevice
│ │ ├── SyncDevice.module.scss
│ │ └── SyncDevice.jsx
│ ├── index.js
│ ├── QRCodeModal
│ │ ├── QRCodeModal.module.scss
│ │ └── QRCodeModal.jsx
│ ├── ConfirmDialog
│ │ ├── ConfirmDialog.module.scss
│ │ └── ConfirmDialog.jsx
│ ├── Signup
│ │ └── Signup.module.scss
│ └── AuthApp
│ │ └── AuthApp.module.scss
├── i18n.js
├── index.js
├── actions
│ └── index.js
├── serviceWorker.js
└── App.jsx
├── .travis.yml
├── public
├── favicon.png
├── manifest.json
└── index.html
├── .storybook
├── preview-head.html
├── addons.js
├── config.js
└── webpack.config.js
├── .env
├── .env.production
├── .eslintrc.js
├── stories
├── Landing.stories.js
├── Loader.stories.js
├── CircleIndicator.stories.js
├── Icons.stories.js
├── NotificationBase.stories.js
├── Typography.stories.js
├── PasswordStrength.stories.js
├── Avatar.stories.js
├── TextField.stories.js
├── Button.stories.js
├── Card.stories.js
└── Switch.stories.js
├── .gitignore
├── i18next-scanner.config.js
├── karma.conf.js
├── test
├── browser.test.js
└── validators.test.js
├── README.md
└── package.json
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | export { default as Masq } from './masq'
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | language: node_js
3 | node_js:
4 | - "lts/*"
5 |
--------------------------------------------------------------------------------
/src/components/Card/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Card'
2 |
--------------------------------------------------------------------------------
/src/components/Modal/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Modal'
2 |
--------------------------------------------------------------------------------
/src/containers/Login/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Login'
2 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Avatar'
2 |
--------------------------------------------------------------------------------
/src/components/Button/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Button'
2 |
--------------------------------------------------------------------------------
/src/components/Dropdown/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Dropdown'
2 |
--------------------------------------------------------------------------------
/src/components/Loader/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Loader'
2 |
--------------------------------------------------------------------------------
/src/components/Switch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Switch'
2 |
--------------------------------------------------------------------------------
/src/containers/Devices/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Devices'
2 |
--------------------------------------------------------------------------------
/src/containers/Landing/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Landing'
2 |
--------------------------------------------------------------------------------
/src/containers/Loading/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Loading'
2 |
--------------------------------------------------------------------------------
/src/containers/Navbar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Navbar'
2 |
--------------------------------------------------------------------------------
/src/containers/Profile/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Profile'
2 |
--------------------------------------------------------------------------------
/src/components/TextField/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './TextField'
2 |
--------------------------------------------------------------------------------
/src/components/Typography/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Typography'
2 |
--------------------------------------------------------------------------------
/src/components/Notification/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Notification'
2 |
--------------------------------------------------------------------------------
/src/containers/Applications/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Applications'
2 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deiu/masq-app/master/public/favicon.png
--------------------------------------------------------------------------------
/src/components/CircleIndicator/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './CircleIndicator'
2 |
--------------------------------------------------------------------------------
/src/components/PasswordStrength/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PasswordStrength'
2 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $mobile-width: 700px;
2 |
3 | :export { mobileWidth: $mobile-width }
4 |
--------------------------------------------------------------------------------
/src/assets/masq_loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deiu/masq-app/master/src/assets/masq_loading.gif
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-links/register';
3 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_SIGNALHUB_URLS='localhost:8080'
2 | REACT_APP_REMOTE_WEBRTC=false
3 | # REACT_APP_STUN_URLS
4 | # REACT_APP_TURN_URLS
5 |
--------------------------------------------------------------------------------
/src/components/Notification/Notification.module.scss:
--------------------------------------------------------------------------------
1 | .Notification {
2 | position: absolute;
3 | z-index: 3;
4 | width: 100%;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/CircleIndicator/CircleIndicator.module.scss:
--------------------------------------------------------------------------------
1 | .CircleIndicator {
2 | width: 4px;
3 | height: 4px;
4 | border-radius: 50%;
5 | }
6 |
--------------------------------------------------------------------------------
/src/hooks/index.js:
--------------------------------------------------------------------------------
1 | export { default as useWindowWidth } from './useWindowWidth'
2 | export { default as useWindowHeight } from './useWindowHeight'
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_SIGNALHUB_URLS='wss://signalhub-jvunerwwrg.now.sh'
2 | REACT_APP_REMOTE_WEBRTC=false
3 | # REACT_APP_STUN_URLS
4 | # REACT_APP_TURN_URLS
5 |
--------------------------------------------------------------------------------
/src/App.module.scss:
--------------------------------------------------------------------------------
1 | @import './styles/variables.scss';
2 |
3 | .layout {
4 | display: flex;
5 | @media (max-width: $mobile-width) {
6 | display: block;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin font($size: 12px, $height: 1.25, $weight: 600, $spacing: normal) {
2 | font-size: $size;
3 | font-weight: $weight;
4 | letter-spacing: $spacing;
5 | line-height: $height;
6 | margin: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/bg-landing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import masq from './masq'
3 | import notification from './notification'
4 | import loading from './loading'
5 |
6 | export default combineReducers({
7 | masq,
8 | notification,
9 | loading
10 | })
11 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './colors.scss';
2 |
3 | body {
4 | margin: 0;
5 | padding: 0;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | background-color: $color-background;
9 | * {
10 | font-family: Asap;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['standard', 'standard-react'],
3 | plugins: ['mocha'],
4 | env: {
5 | 'mocha': true
6 | },
7 | overrides: [{
8 | files: '*.test.js',
9 | rules: {
10 | 'no-unused-expressions': 'off'
11 | }
12 | }]
13 | }
14 |
--------------------------------------------------------------------------------
/src/modals/PersistentStorageRequest/PersistentStorageRequest.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .PermanentStorage {
4 | display: flex;
5 | height: 100%;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: space-between;
9 | text-align: center;
10 | }
11 |
--------------------------------------------------------------------------------
/src/reducers/loading.js:
--------------------------------------------------------------------------------
1 | const loading = (state = {
2 | loading: false
3 | }, action) => {
4 | switch (action.type) {
5 | case 'SET_LOADING':
6 | return {
7 | ...state,
8 | loading: action.loading
9 | }
10 | default:
11 | return state
12 | }
13 | }
14 |
15 | export default loading
16 |
--------------------------------------------------------------------------------
/src/assets/check-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Loader/Loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import styles from './Loader.module.scss'
4 |
5 | const Loader = () => (
6 |
11 | )
12 |
13 | export default Loader
14 |
--------------------------------------------------------------------------------
/src/modals/UnsupportedBrowser/UnsupportedBrowser.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .UnsupportedBrowser {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: space-between;
8 | text-align: center;
9 | height: 100%;
10 |
11 | .fontMedium {
12 | font-weight: 500;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/stories/Landing.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { Landing } from '../src/containers'
5 |
6 | Landing.displayName = 'Landing'
7 |
8 | storiesOf('Landing', module)
9 | .add('landing', () => (
10 |
11 |
12 |
13 | ))
14 |
--------------------------------------------------------------------------------
/src/containers/index.js:
--------------------------------------------------------------------------------
1 | export { default as Login } from './Login'
2 | export { default as Applications } from './Applications'
3 | export { default as Navbar } from './Navbar'
4 | export { default as Profile } from './Profile'
5 | export { default as Devices } from './Devices'
6 | export { default as Landing } from './Landing'
7 | export { default as Loading } from './Loading'
8 |
--------------------------------------------------------------------------------
/src/components/Card/Image.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styles from './Card.module.scss'
5 |
6 | const Image = ({ image }) => (
7 |
8 | )
9 |
10 | Image.propTypes = {
11 | image: PropTypes.string.isRequired
12 | }
13 |
14 | export default Image
15 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/stories/Loader.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { Loader } from '../src/components'
5 |
6 | Loader.displayName = 'Loader'
7 |
8 | storiesOf('Loader', module)
9 | .addParameters({
10 | info: 'A Loader to show that an operation is in progress.'
11 | })
12 | .add('loader', () => (
13 |
14 | ))
15 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react'
2 |
3 | import 'typeface-asap'
4 | import 'typeface-asap-condensed'
5 |
6 | // automatically import all files ending in *.stories.js
7 | const req = require.context('../stories', true, /.stories.js$/)
8 | function loadStories() {
9 | req.keys().forEach(filename => req(filename))
10 | }
11 |
12 | configure(loadStories, module)
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | .vscode/
3 |
4 | # dependencies
5 | /node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/src/reducers/notification.js:
--------------------------------------------------------------------------------
1 | const notification = (state = {
2 | currentNotification: null // { title: 'test', error: true }
3 | }, action) => {
4 | switch (action.type) {
5 | case 'SET_NOTIFICATION':
6 | return {
7 | ...state,
8 | currentNotification: action.notification
9 | }
10 | default:
11 | return state
12 | }
13 | }
14 |
15 | export default notification
16 |
--------------------------------------------------------------------------------
/src/components/CircleIndicator/CircleIndicator.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styles from './CircleIndicator.module.scss'
5 |
6 | const CircleIndicator = ({ color }) => (
7 |
8 | )
9 |
10 | CircleIndicator.propTypes = {
11 | color: PropTypes.string.isRequired
12 | }
13 |
14 | export default CircleIndicator
15 |
--------------------------------------------------------------------------------
/stories/CircleIndicator.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { CircleIndicator } from '../src/components'
5 |
6 | CircleIndicator.displayName = 'CircleIndicator'
7 |
8 | storiesOf('CircleIndicator', module)
9 | .addParameters({
10 | info: 'A simple colored circle to indicate a status'
11 | })
12 | .add('circleIndicator', () => (
13 |
14 | ))
15 |
--------------------------------------------------------------------------------
/src/containers/Loading/Loading.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Login {
4 | background-color: $color-dark-blue-100;
5 | position: relative;
6 | min-height: 100vh;
7 | display: flex;
8 | justify-content: space-between;
9 | flex-direction: column;
10 | align-items: center;
11 | text-align: center;
12 | overflow: hidden;
13 |
14 | .Background {
15 | background-color: $color-dark-blue-100;
16 | flex-shrink: 0;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/hooks/useWindowWidth.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | const useWindowWidth = () => {
4 | const [width, setWidth] = useState(window.innerWidth)
5 |
6 | useEffect(() => {
7 | const handleResize = () => setWidth(window.innerWidth)
8 | window.addEventListener('resize', handleResize)
9 | return () => {
10 | window.removeEventListener('resize', handleResize)
11 | }
12 | })
13 |
14 | return width
15 | }
16 |
17 | export default useWindowWidth
18 |
--------------------------------------------------------------------------------
/src/hooks/useWindowHeight.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | const useWindowHeight = () => {
4 | const [width, setWidth] = useState(window.innerHeight)
5 |
6 | useEffect(() => {
7 | const handleResize = () => setWidth(window.innerHeight)
8 | window.addEventListener('resize', handleResize)
9 | return () => {
10 | window.removeEventListener('resize', handleResize)
11 | }
12 | })
13 |
14 | return width
15 | }
16 |
17 | export default useWindowHeight
18 |
--------------------------------------------------------------------------------
/src/components/Dropdown/Dropdown.module.scss:
--------------------------------------------------------------------------------
1 | .Dropdown {
2 | display: flex;
3 | width: 150px;
4 | right: 14px;
5 | align-items: center;
6 | top: 64px;
7 | position: absolute;
8 | border-radius: 3px;
9 | background-color: white;
10 | height: 35px;
11 | z-index: 1;
12 | padding-left: 12px;
13 |
14 | font-size: 13px;
15 | line-height: 1.23;
16 | color: #353c52;
17 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
18 |
19 | :first-child {
20 | margin-right: 8px;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | // Export a function. Accept the base config as the only param.
4 | module.exports = async ({ config, mode }) => {
5 | // `mode` has a value of 'DEVELOPMENT' or 'PRODUCTION'
6 | // You can change the configuration based on that.
7 | // 'PRODUCTION' is used when building the static version of storybook.
8 |
9 | // Make whatever fine-grained changes you need
10 | config.node = {
11 | fs: 'empty'
12 | }
13 |
14 | // Return the altered config
15 | return config
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Space.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { capitalize } from '../lib/utils'
5 |
6 | const Space = ({ size, direction }) => (
7 |
8 | )
9 |
10 | Space.defaultProps = {
11 | direction: 'bottom'
12 | }
13 |
14 | Space.propTypes = {
15 | size: PropTypes.number.isRequired,
16 | direction: PropTypes.oneOf([
17 | 'top',
18 | 'right',
19 | 'bottom',
20 | 'left'
21 | ])
22 | }
23 |
24 | export default Space
25 |
--------------------------------------------------------------------------------
/stories/Icons.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { Camera, Paperclip, Trash, Phone, Settings, Grid, List, LogOut } from 'react-feather'
4 |
5 | storiesOf('Icons', module)
6 | .addParameters({
7 | info: 'Some icons from feather used across Masq'
8 | })
9 | .add('feather icons', () => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ))
21 |
--------------------------------------------------------------------------------
/src/components/Dropdown/Dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { LogOut } from 'react-feather'
3 | import PropTypes from 'prop-types'
4 |
5 | import { useTranslation } from 'react-i18next'
6 | import styles from './Dropdown.module.scss'
7 |
8 | const Dropdown = ({ onClick }) => {
9 | const { t } = useTranslation()
10 | return (
11 |
12 |
13 |
{t('Sign out')}
14 |
15 | )
16 | }
17 |
18 | Dropdown.propTypes = {
19 | onClick: PropTypes.func
20 | }
21 |
22 | export default Dropdown
23 |
--------------------------------------------------------------------------------
/src/modals/PasswordEdit/PasswordEdit.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .PasswordEdit {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: space-between;
9 | height: 100%;
10 | }
11 |
12 | .buttons {
13 | @media (min-width: $mobile-width + 1px) {
14 | >:first-child {
15 | margin-right: 16px;
16 | }
17 | }
18 |
19 | @media (max-width: $mobile-width) {
20 | display: flex;
21 | flex-direction: column-reverse;
22 | >:first-child {
23 | margin-top: 12px;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/containers/Devices/Devices.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/variables.scss';
2 |
3 | .Devices {
4 | width: 100%;
5 | margin: 48px 48px 48px;
6 |
7 | @media (max-width: $mobile-width) {
8 | margin: 16px;
9 | width: unset;
10 | }
11 |
12 | .topSection {
13 | display: flex;
14 | height: 50px;
15 | justify-content: space-between;
16 | }
17 |
18 | .cards {
19 | display: grid;
20 | grid-gap: 28px;
21 | @media (min-width: 1200px) {
22 | margin-right: 365px;
23 | }
24 |
25 | @media (min-width: 1400px) {
26 | grid-template-columns: repeat(2, 1fr);
27 | }
28 | }
29 |
30 | .sidebar {
31 | text-align: left;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/stories/NotificationBase.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 |
5 | import NotificationBase from '../src/components/Notification/NotificationBase.jsx'
6 |
7 | NotificationBase.displayName = 'NotificationBase'
8 |
9 | storiesOf('NotificationBase', module)
10 | .addParameters({
11 | info: 'Notification to alerts of success/error events.'
12 | })
13 | .add('default success', () => (
14 |
15 | ))
16 | .add('error', () => (
17 |
18 | ))
19 |
--------------------------------------------------------------------------------
/src/containers/Navbar/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import MediaQuery from 'react-responsive'
3 |
4 | import NavbarMin from './NavbarMin'
5 | import NavbarMax from './NavbarMax'
6 | import NavBarMobile from './NavbarMobile'
7 |
8 | const Navbar = (props) => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default Navbar
25 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Avatar } from './Avatar'
2 | export { default as Button } from './Button'
3 | export { default as Card } from './Card'
4 | export { default as CircleIndicator } from './CircleIndicator'
5 | export { default as Dropdown } from './Dropdown'
6 | export { default as Loader } from './Loader'
7 | export { default as Modal } from './Modal'
8 | export { default as Notification } from './Notification'
9 | export { default as Switch } from './Switch'
10 | export { default as TextField } from './TextField'
11 | export { default as Typography } from './Typography'
12 | export { default as Space } from './Space'
13 | export { default as PasswordStrength } from './PasswordStrength'
14 |
--------------------------------------------------------------------------------
/src/modals/DeleteAppDialog/DeleteAppDialog.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .ConfirmDialog {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: space-between;
9 | text-align: center;
10 | height: 100%;
11 |
12 | .buttons {
13 | display: flex;
14 | width: 100%;
15 | align-items: center;
16 | @media (min-width: $mobile-width + 1px) {
17 | justify-content: space-between;
18 | }
19 |
20 | @media (max-width: $mobile-width) {
21 | display: flex;
22 | flex-direction: column;
23 | >:first-child {
24 | margin-bottom: 12px;
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Switch/SwitchButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import cx from 'classnames'
3 | import PropTypes from 'prop-types'
4 |
5 | import styles from './Switch.module.scss'
6 |
7 | const SwitchButton = ({ checked, secondary, onChange, color }) => (
8 |
9 |
10 |
11 |
12 | )
13 |
14 | SwitchButton.propTypes = {
15 | checked: PropTypes.bool,
16 | secondary: PropTypes.bool,
17 | onChange: PropTypes.func,
18 | color: PropTypes.string.isRequired
19 | }
20 |
21 | export default SwitchButton
22 |
--------------------------------------------------------------------------------
/src/modals/DeleteProfileDialog/DeleteProfileDialog.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .ConfirmDialog {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: space-between;
9 | text-align: center;
10 | height: 100%;
11 |
12 | .buttons {
13 | display: flex;
14 | width: 100%;
15 | align-items: center;
16 | @media (min-width: $mobile-width + 1px) {
17 | justify-content: space-between;
18 | }
19 |
20 | @media (max-width: $mobile-width) {
21 | display: flex;
22 | flex-direction: column;
23 | >:first-child {
24 | margin-bottom: 12px;
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/assets/plus-square.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/browser.js:
--------------------------------------------------------------------------------
1 | import { detect } from 'detect-browser'
2 |
3 | const SUPPORTED_BROWSERS_CODES = [
4 | 'firefox',
5 | 'chrome',
6 | 'safari',
7 | 'ios',
8 | 'android',
9 | 'crios',
10 | 'fxios',
11 | 'samsung'
12 | ]
13 |
14 | // Recommended browsers displayed in the modal
15 | // if the current browser is unsupported
16 | const SUPPORTED_BROWSERS = [
17 | 'firefox',
18 | 'brave', // brave will be detected as chrome
19 | 'chrome',
20 | 'safari'
21 | ]
22 |
23 | const isBrowserSupported = () => {
24 | const browser = detect()
25 | if (!browser) return false
26 | return !!SUPPORTED_BROWSERS_CODES.includes(browser.name)
27 | }
28 |
29 | export {
30 | isBrowserSupported,
31 | SUPPORTED_BROWSERS,
32 | SUPPORTED_BROWSERS_CODES
33 | }
34 |
--------------------------------------------------------------------------------
/src/modals/SyncDevice/SyncDevice.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .SyncDevice {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | height: 100%;
8 | width: 100%;
9 |
10 | input {
11 | font-size: 14px;
12 | font-weight: 600;
13 | letter-spacing: 0.7px;
14 | text-align: center;
15 | color: $color-blue;
16 | margin-bottom: 48px;
17 | outline: none;
18 | border: 0;
19 | }
20 |
21 | .loader {
22 | margin-top: 32px;
23 | }
24 |
25 | .refeshIcon {
26 | animation: rotation 3s infinite linear;
27 | }
28 |
29 | @keyframes rotation {
30 | from {
31 | transform: rotate(0deg);
32 | }
33 | to {
34 | transform: rotate(359deg);
35 | }
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/modals/index.js:
--------------------------------------------------------------------------------
1 | export { default as Signup } from './Signup/Signup'
2 | export { default as AuthApp } from './AuthApp/AuthApp'
3 | export { default as QRCodeModal } from './QRCodeModal/QRCodeModal'
4 | export { default as SyncDevice } from './SyncDevice/SyncDevice'
5 | export { default as PersistentStorageRequest } from './PersistentStorageRequest/PersistentStorageRequest'
6 | export { default as ConfirmDialog } from './ConfirmDialog/ConfirmDialog'
7 | export { default as DeleteProfileDialog } from './DeleteProfileDialog/DeleteProfileDialog'
8 | export { default as DeleteAppDialog } from './DeleteAppDialog/DeleteAppDialog'
9 | export { default as PasswordEdit } from './PasswordEdit/PasswordEdit'
10 | export { default as UnsupportedBrowser } from './UnsupportedBrowser/UnsupportedBrowser'
11 |
--------------------------------------------------------------------------------
/src/modals/QRCodeModal/QRCodeModal.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .QRCode {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | height: 100%;
8 | width: 100%;
9 |
10 | input {
11 | font-size: 14px;
12 | font-weight: 600;
13 | }
14 |
15 | .input {
16 | input {
17 | color: $color-blue;
18 | }
19 |
20 | }
21 | .copied {
22 | input {
23 | color: white;
24 | background-color: $color-blue;
25 | border: solid 1px #353c52;
26 | }
27 | svg {
28 | color: white;
29 | }
30 | }
31 | }
32 |
33 | .pill {
34 | height: 28px;
35 | width: 120px;
36 | line-height: 28px;
37 | background-color: $color-green;
38 | color: white;
39 | border-radius: 50px;
40 | font-size: 14px;
41 | text-align: center;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Loader/Loader.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | $color-blue-grey: #697496;
4 |
5 | @keyframes dot-keyframes {
6 | 0% {
7 | opacity: 1;
8 | }
9 | 50% {
10 | opacity: 0;
11 | }
12 | 100% {
13 | opacity: 1;
14 | }
15 | }
16 |
17 | .Loader {
18 | display: flex;
19 |
20 | .dot {
21 | width: 16px;
22 | height: 16px;
23 | border-radius: 100%;
24 | animation: dot-keyframes 2s infinite ease-in-out;
25 |
26 | &:nth-child(1) {
27 | margin-right: 10px;
28 | background-color: black;
29 | }
30 |
31 | &:nth-child(2) {
32 | margin-right: 10px;
33 | background-color: black;
34 | animation-delay: .5s
35 | }
36 |
37 | &:nth-child(3) {
38 | background-color: black;
39 | animation-delay: 1s;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/containers/Applications/Link.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { ExternalLink } from 'react-feather'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | import { Typography } from '../../components'
7 | import styles from './Applications.module.scss'
8 |
9 | const Link = ({ url, label }) => {
10 | const { t } = useTranslation()
11 |
12 | return (
13 |
19 | )
20 | }
21 |
22 | Link.propTypes = {
23 | url: PropTypes.string,
24 | label: PropTypes.string
25 | }
26 |
27 | export default Link
28 |
--------------------------------------------------------------------------------
/src/modals/ConfirmDialog/ConfirmDialog.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .ConfirmDialog {
4 | display: flex;
5 | flex-direction: column;
6 | align-items: center;
7 | justify-content: center;
8 | text-align: center;
9 | margin-top: 80px;
10 |
11 | .title {
12 | font-size: 24px;
13 | font-weight: bold;
14 | line-height: 1.04;
15 | text-align: center;
16 | margin-bottom: 32px;
17 | color: $color-dark-blue-100;
18 | }
19 |
20 | p {
21 | width: 400px;
22 | font-size: 14px;
23 | font-weight: 500;
24 | line-height: 1.57;
25 | margin-bottom: 48px;
26 | }
27 |
28 | Button {
29 | width: 185px;
30 | margin-right: 16px;
31 | margin-left: 16px;
32 | }
33 |
34 | .buttons {
35 | width: 100%;
36 | display: flex;
37 | justify-content: space-evenly;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/i18next-scanner.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | input: [
3 | 'src/**/*.{js,jsx}',
4 | // Use ! to filter out files or directories
5 | '!**/node_modules/**'
6 | ],
7 | options: {
8 | debug: true,
9 | removeUnusedKeys: true,
10 | func: {
11 | list: ['i18next.t', 'i18n.t', 't'],
12 | extensions: ['.js', '.jsx']
13 | },
14 | lngs: ['en', 'fr'],
15 | defaultLng: 'en',
16 | defaultNs: 'resource',
17 | defaultValue: '__STRING_NOT_TRANSLATED__',
18 | resource: {
19 | loadPath: 'public/locales/{{lng}}.json',
20 | savePath: 'public/locales/{{lng}}.json',
21 | jsonIndent: 2,
22 | lineEnding: '\n'
23 | },
24 | nsSeparator: false, // namespace separator
25 | keySeparator: false, // key separator
26 | interpolation: {
27 | prefix: '{{',
28 | suffix: '}}'
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | process.env.CHROME_BIN = require('puppeteer').executablePath()
2 |
3 | module.exports = function (config) {
4 | config.set({
5 | frameworks: ['mocha', 'chai'],
6 | files: ['test/**/*.js'],
7 | reporters: ['progress'],
8 | port: 9876, // karma web server port
9 | colors: true,
10 | logLevel: config.LOG_INFO,
11 | browsers: ['ChromeHeadless'],
12 | autoWatch: false,
13 | singleRun: true,
14 | concurrency: Infinity,
15 | preprocessors: {
16 | 'test/**/*.js': [ 'webpack', 'sourcemap' ]
17 | },
18 | webpack: {
19 | mode: 'development',
20 | node: {
21 | fs: 'empty'
22 | }
23 | },
24 | webpackMiddleware: {
25 | stats: 'errors-only',
26 | devtool: 'inline-source-map'
27 | },
28 | browserDisconnectTimeout: 60000,
29 | browserNoActivityTimeout: 60000
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Card/Card.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/mixins.scss';
2 | @import '../../styles/colors.scss';
3 |
4 | .Card {
5 | border: solid 1px $color-grey-200;
6 | border-radius: 3px;
7 | background-color: white;
8 | width: 100%;
9 |
10 | .content {
11 | margin-left: 24px;
12 | margin-right: 24px;
13 | margin-bottom: 24px;
14 | }
15 |
16 | .Image {
17 | height: 58px;
18 | background-repeat: no-repeat;
19 | background-position: center;
20 | background-size: cover;
21 | }
22 |
23 | .Header {
24 | display: flex;
25 | align-items: center;
26 | justify-content: space-between;
27 |
28 | .marker {
29 | margin-top: 24px;
30 | margin-bottom: 16px;
31 | width: 40px;
32 | height: 4px;
33 | border-radius: 2px;
34 | }
35 | }
36 |
37 | .Description {
38 | margin-top: 8px;
39 | margin-bottom: 16px;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Notification/NotificationBase.module.scss:
--------------------------------------------------------------------------------
1 | .Notification {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | height: 48px;
6 | background-color: #495063;
7 |
8 | .title {
9 | margin: auto;
10 | display: flex;
11 | align-items: center;
12 |
13 | p {
14 | color: white;
15 | font-size: 14px;
16 | font-weight: 500;
17 | font-style: normal;
18 | font-stretch: normal;
19 | line-height: 1.21;
20 | letter-spacing: normal;
21 | }
22 | }
23 |
24 | .iconContainer {
25 | height: 16px;
26 | width: 16px;
27 | margin-right: 16px;
28 | border-radius: 100%;
29 | display: flex;
30 | align-items: center;
31 | justify-content: center;
32 | background-color: white;
33 | }
34 |
35 | .closeBtn {
36 | margin-right: 20px;
37 | color: white;
38 | cursor: pointer;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/containers/Loading/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useTranslation } from 'react-i18next'
3 | import { ReactComponent as Cubes } from '../../assets/cubes.svg'
4 | import { ReactComponent as Logo } from '../../assets/logo.svg'
5 | import { Space, Typography } from '../../components'
6 | import animation from '../../assets/masq_loading.gif'
7 |
8 | import styles from './Loading.module.scss'
9 |
10 | const Loading = () => {
11 | const { t } = useTranslation()
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
{t('Loading...')}
21 |
22 |
23 | )
24 | }
25 |
26 | export default Loading
27 |
--------------------------------------------------------------------------------
/src/components/Notification/NotificationBase.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Check, X } from 'react-feather'
5 |
6 | import styles from './NotificationBase.module.scss'
7 |
8 | const NotificationBase = ({ error, title, onClose }) => {
9 | const Icon = error
10 | ?
11 | :
12 |
13 | return (
14 |
15 |
16 |
17 | {Icon}
18 |
19 |
{title}
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | NotificationBase.propTypes = {
27 | error: PropTypes.bool,
28 | onClose: PropTypes.func,
29 | title: PropTypes.string.isRequired
30 | }
31 |
32 | export default NotificationBase
33 |
--------------------------------------------------------------------------------
/src/modals/Signup/Signup.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .Signup {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: space-between;
9 | min-height: 100%;
10 |
11 | .avatar {
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | }
16 | }
17 |
18 | .user {
19 | font-size: 18px;
20 | font-weight: 600;
21 | line-height: 1.11;
22 | text-align: center;
23 | color: $color-dark-blue-100 !important;
24 | }
25 |
26 | .buttons {
27 | @media (min-width: $mobile-width + 1px) {
28 | >:nth-child(2) {
29 | margin-left: 12px;
30 | }
31 | }
32 |
33 | @media (max-width: $mobile-width) {
34 | display: flex;
35 | flex-direction: column-reverse;
36 | >:first-child {
37 | margin-top: 12px;
38 | }
39 | }
40 | }
41 |
42 | .TextField {
43 | text-align: left;
44 | }
45 |
46 | .fontMedium {
47 | font-weight: 500;
48 | }
49 |
--------------------------------------------------------------------------------
/src/modals/AuthApp/AuthApp.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .AuthApp {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | height: 100%;
9 | width: 100%;
10 | text-align: center;
11 | justify-content: space-between;
12 |
13 | .appTitle {
14 | font-size: 32px;
15 | font-weight: bold;
16 | letter-spacing: 0.2px;
17 | text-align: center;
18 | color: $color-dark-blue-100;
19 | margin: 0;
20 | margin-bottom: 16px;
21 | }
22 |
23 | .buttons {
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | width: 100%;
28 | @media (min-width: $mobile-width + 1px) {
29 | justify-content: space-between;
30 | }
31 |
32 | @media (max-width: $mobile-width) {
33 | margin-top: 0;
34 | display: flex;
35 | flex-direction: column-reverse;
36 | >:first-child {
37 | margin-top: 12px;
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/colors.scss:
--------------------------------------------------------------------------------
1 | $color-grey-100: #f4f6fa;
2 | $color-grey-200: #e0e1e6;
3 | $color-grey-300: #c8cbd3;
4 | :export { colorGrey100: $color-grey-100 }
5 | :export { colorGrey200: $color-grey-200 }
6 | :export { colorGrey300: $color-grey-300; }
7 |
8 | $color-dark-blue-100: #252a39;
9 | $color-dark-blue-200: #171c2f;
10 | :export { colorDarkBlue100: $color-dark-blue-100; }
11 | :export { colorDarkBlue200: $color-dark-blue-200; }
12 |
13 | $color-cyan: #00cafc;
14 | $color-blue: #03a0f9;
15 | $color-green: #40ae6c;
16 | $color-red: #e53b5b;
17 | $color-blue-grey: #5c6f84;
18 | $color-yellow: #f3b100;
19 | $color-purple: #a3005c;
20 | :export { colorCyan: $color-cyan }
21 | :export { colorBlue: $color-blue; }
22 | :export { colorGreen: $color-green; }
23 | :export { colorRed: $color-red; }
24 | :export { colorBlueGrey: $color-blue-grey; }
25 | :export { colorYellow: $color-yellow; }
26 | :export { colorPurple: $color-purple; }
27 |
28 | $color-background: $color-grey-100;
29 | :export { colorBackground: $color-background; }
30 |
--------------------------------------------------------------------------------
/stories/Typography.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { Typography } from '../src/components'
5 |
6 | Typography.displayName = 'Typography'
7 |
8 | storiesOf('Typography', module)
9 | .addParameters({
10 | info: 'Typography'
11 | })
12 | .add('titleModal', () => (
13 | Title modal
14 | ))
15 | .add('title', () => (
16 | Title
17 | ))
18 | .add('paragraph', () => (
19 | Text
20 | ))
21 | .add('paragraph-modal', () => (
22 | Text
23 | ))
24 | .add('username', () => (
25 | Username
26 | ))
27 | .add('label', () => (
28 | Label
29 | ))
30 | .add('label-nav', () => (
31 | Label nav
32 | ))
33 |
--------------------------------------------------------------------------------
/src/lib/validators.js:
--------------------------------------------------------------------------------
1 | const isName = str => /^$|^[A-zÀ-ú\- ]+$/.test(str)
2 |
3 | const isUsername = str => /^[\w!?$#@()\-*]+$/.test(str)
4 |
5 | const getForce = str => {
6 | const info = getPasswordInfo(str)
7 | return str.length < 6 ? 0 : computeScore(info)
8 | }
9 |
10 | const isForceEnough = (str) => getForce(str) > 1
11 |
12 | const containUppercase = str => /[A-Z]/.test(str)
13 |
14 | const containLowercase = str => /[a-z]/.test(str)
15 |
16 | const containNumber = str => /[0-9]/.test(str)
17 |
18 | const containSpecialCharacter = str => /[="!?$#%@()\\\-_/@^+*&:<>{};~'`.|[\]]/.test(str)
19 |
20 | const getPasswordInfo = str => ({
21 | lowercase: containLowercase(str),
22 | uppercase: containUppercase(str),
23 | number: containNumber(str),
24 | specialCharacter: containSpecialCharacter(str),
25 | secureLength: str.length >= 12
26 | })
27 |
28 | const computeScore = info => Object.keys(info).reduce((acc, cur) => acc + (info[cur] ? 1 : 0), 0)
29 |
30 | export { isName, isUsername, getPasswordInfo, getForce, isForceEnough }
31 |
--------------------------------------------------------------------------------
/stories/PasswordStrength.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { PasswordStrength } from '../src/components'
5 |
6 | storiesOf('PasswordStrength', module)
7 | .addParameters({
8 | info: 'A Card can contain two children: actions displayed in the top right corner, and a footer.'
9 | })
10 | .add('an empty password', () => (
11 |
12 | ))
13 | .add('a password with only lowercases: ag', () => (
14 |
15 | ))
16 | .add('a password with also an uppercase : agzE', () => (
17 |
18 | ))
19 | .add('a password with also a number : agzE28', () => (
20 |
21 | ))
22 | .add('a password with also a special character : agzE28#', () => (
23 |
24 | ))
25 | .add('a password like : agtyzE28#58956', () => (
26 |
27 | ))
28 |
--------------------------------------------------------------------------------
/src/components/Notification/Notification.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 |
5 | import NotificationBase from './NotificationBase'
6 | import { setNotification } from '../../actions/index'
7 |
8 | import styles from './Notification.module.scss'
9 |
10 | const TIMEOUT = 5000
11 |
12 | const Notification = ({ setNotification, ...props }) => {
13 | if (!props.error) {
14 | // Close notification after timeout only if it is not an error
15 | setTimeout(() => setNotification(null), TIMEOUT)
16 | }
17 |
18 | return (
19 |
20 | setNotification(null)} />
21 |
22 | )
23 | }
24 |
25 | const mapDispatchToProps = dispatch => ({
26 | setNotification: notif => dispatch(setNotification(notif))
27 | })
28 |
29 | Notification.propTypes = {
30 | setNotification: PropTypes.func,
31 | error: PropTypes.bool
32 | }
33 |
34 | export default connect(null, mapDispatchToProps)(Notification)
35 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Typography from '../Typography'
5 | import SwitchButton from './SwitchButton'
6 |
7 | import styles from './Switch.module.scss'
8 |
9 | const Switch = (props) => (
10 | props.label
11 | ? (
12 |
13 | {props.label}
14 |
15 |
16 | )
17 | :
18 | )
19 |
20 | Switch.defaultProps = {
21 | checked: false,
22 | secondary: false,
23 | label: ''
24 | }
25 |
26 | Switch.propTypes = {
27 | /** Initial checked state */
28 | checked: PropTypes.bool,
29 |
30 | /** secondary style */
31 | secondary: PropTypes.bool,
32 |
33 | /** Label to display */
34 | label: PropTypes.string,
35 |
36 | /** Current color */
37 | color: PropTypes.string.isRequired,
38 |
39 | /** onChange handler */
40 | onChange: PropTypes.func
41 | }
42 |
43 | export default Switch
44 |
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next'
2 | import Backend from 'i18next-xhr-backend'
3 | import LanguageDetector from 'i18next-browser-languagedetector'
4 | import { initReactI18next } from 'react-i18next'
5 |
6 | i18n
7 | // load translation using xhr -> see /public/locales
8 | // learn more: https://github.com/i18next/i18next-xhr-backend
9 | .use(Backend)
10 | // detect user language
11 | // learn more: https://github.com/i18next/i18next-browser-languageDetector
12 | .use(LanguageDetector)
13 | // pass the i18n instance to react-i18next.
14 | .use(initReactI18next)
15 | // init i18next
16 | // for all options read: https://www.i18next.com/overview/configuration-options
17 | .init({
18 | fallbackLng: 'en',
19 | nsSeparator: false,
20 | keySeparator: false,
21 | debug: process.env.NODE_ENV === 'development',
22 | load: 'languageOnly', // en, fr ...
23 | interpolation: {
24 | escapeValue: false // not needed for react as it escapes by default
25 | },
26 | backend: {
27 | loadPath: '/locales/{{lng}}.json'
28 | }
29 | })
30 |
31 | export default i18n
32 |
--------------------------------------------------------------------------------
/src/reducers/masq.js:
--------------------------------------------------------------------------------
1 | const masq = (state = {
2 | syncStep: 0,
3 | users: [],
4 | apps: [],
5 | devices: [],
6 | currentUser: null,
7 | currentAppRequest: null
8 | }, action) => {
9 | switch (action.type) {
10 | case 'SIGNIN':
11 | return { ...state, currentUser: action.profile }
12 | case 'SIGNOUT':
13 | return { ...state, currentUser: null }
14 | case 'RECEIVE_USERS':
15 | return { ...state, users: action.users }
16 | case 'RECEIVE_APPS':
17 | return { ...state, apps: action.apps }
18 | case 'SET_CURRENT_APP_REQUEST':
19 | return { ...state, currentAppRequest: action.app }
20 | case 'UPDATE_CURRENT_APP_REQUEST':
21 | return { ...state, currentAppRequest: { ...state.currentAppRequest, ...action.update } }
22 | case 'ADD_APP':
23 | return { ...state, apps: [...state.apps, action.app] }
24 | case 'ADD_DEVICE':
25 | return { ...state, devices: [...state.devices, action.device] }
26 | case 'SET_SYNC_STEP':
27 | return { ...state, syncStep: action.syncStep }
28 | default:
29 | return state
30 | }
31 | }
32 |
33 | export default masq
34 |
--------------------------------------------------------------------------------
/src/modals/ConfirmDialog/ConfirmDialog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useTranslation } from 'react-i18next'
4 | import { Button, Modal } from '../../components'
5 |
6 | import styles from './ConfirmDialog.module.scss'
7 |
8 | const ConfirmDialog = ({ title, text, onConfirm, onCancel, onClose }) => {
9 | const { t } = useTranslation()
10 |
11 | return (
12 |
13 |
14 |
{title}
15 |
{text}
16 |
17 | {t('Cancel')}
18 | {t('Confirm')}
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | ConfirmDialog.propTypes = {
26 | title: PropTypes.string.isRequired,
27 | text: PropTypes.string.isRequired,
28 | onConfirm: PropTypes.func.isRequired,
29 | onCancel: PropTypes.func.isRequired,
30 | onClose: PropTypes.func.isRequired
31 | }
32 |
33 | export default ConfirmDialog
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { render } from 'react-dom'
3 | import { createStore, applyMiddleware, compose } from 'redux'
4 | import thunkMiddleware from 'redux-thunk'
5 | import { Provider } from 'react-redux'
6 |
7 | import 'typeface-asap'
8 | import 'typeface-asap-condensed'
9 |
10 | import './i18n'
11 | import * as serviceWorker from './serviceWorker'
12 | import rootReducer from './reducers'
13 | import App from './App'
14 |
15 | import './styles/index.scss'
16 |
17 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
18 |
19 | const store = createStore(
20 | rootReducer,
21 | composeEnhancer(applyMiddleware(thunkMiddleware))
22 | )
23 |
24 | render(
25 |
26 |
27 |
28 |
29 | ,
30 | document.getElementById('root')
31 | )
32 |
33 | // If you want your app to work offline and load faster, you can change
34 | // unregister() to register() below. Note this comes with some pitfalls.
35 | // Learn more about service workers: http://bit.ly/CRA-PWA
36 | serviceWorker.unregister()
37 |
--------------------------------------------------------------------------------
/test/browser.test.js:
--------------------------------------------------------------------------------
1 | import * as sinon from 'sinon'
2 | import DetectBrowser from 'detect-browser'
3 |
4 | import { isBrowserSupported, SUPPORTED_BROWSERS_CODES } from '../src/lib/browser'
5 |
6 | const { expect } = require('chai')
7 |
8 | describe('Browser support', () => {
9 | let stub = null
10 |
11 | const stubBrowser = (name) => {
12 | stub = sinon
13 | .stub(DetectBrowser, 'detect')
14 | .callsFake(() => ({ name }))
15 | }
16 |
17 | afterEach(() => {
18 | stub.restore()
19 | })
20 |
21 | it('should return true for browsers in whitelist', () => {
22 | SUPPORTED_BROWSERS_CODES.forEach((browser) => {
23 | stubBrowser(browser)
24 | expect(isBrowserSupported()).to.be.true
25 | stub.restore()
26 | })
27 | })
28 |
29 | it('should return false for non-whitelisted browsers', () => {
30 | stubBrowser('ie')
31 | expect(isBrowserSupported()).to.be.false
32 | })
33 |
34 | it('should return false for non-detected browsers', () => {
35 | stub = sinon
36 | .stub(DetectBrowser, 'detect')
37 | .callsFake(() => null)
38 |
39 | expect(isBrowserSupported()).to.be.false
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import Compressor from 'compressorjs'
2 |
3 | const MAX_IMAGE_SIZE = 100000 // 100 KB
4 |
5 | const isUsernameAlreadyTaken = (username, id) => {
6 | const ids = Object
7 | .keys(window.localStorage)
8 | .filter(k => k.split('-')[0] === 'profile')
9 |
10 | if (!ids) return false
11 |
12 | const publicProfiles = ids.map(id => JSON.parse(window.localStorage.getItem(id)))
13 |
14 | return publicProfiles.find(p =>
15 | (id && p.id === id) ? false : p.username === username
16 | )
17 | }
18 |
19 | const compressImage = (file) => {
20 | return new Promise((resolve, reject) => {
21 | const image = new Compressor(file, { // eslint-disable-line no-unused-vars
22 | quality: 0.8,
23 | width: 512,
24 | height: 512,
25 | convertSize: 0,
26 | success (result) {
27 | resolve(result)
28 | },
29 | error (err) {
30 | reject(err.message)
31 | }
32 | })
33 | })
34 | }
35 |
36 | const capitalize = (string) => (
37 | string.charAt(0).toUpperCase() + string.slice(1)
38 | )
39 |
40 | export {
41 | isUsernameAlreadyTaken,
42 | compressImage,
43 | capitalize,
44 | MAX_IMAGE_SIZE
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import cx from 'classnames'
4 |
5 | import styles from './Button.module.scss'
6 |
7 | const Button = ({ onClick, color, children, width, height, borderRadius, secondary }) => (
8 |
22 | {children}
23 |
24 | )
25 |
26 | Button.defaultProps = {
27 | color: 'primary',
28 | height: 50,
29 | borderRadius: 6
30 | }
31 |
32 | Button.propTypes = {
33 | onClick: PropTypes.func,
34 | color: PropTypes.oneOf([
35 | 'primary',
36 | 'success',
37 | 'danger',
38 | 'neutral',
39 | 'light'
40 | ]),
41 | width: PropTypes.number,
42 | height: PropTypes.number,
43 | secondary: PropTypes.bool,
44 | children: PropTypes.string,
45 | borderRadius: PropTypes.number
46 | }
47 |
48 | export default Button
49 |
--------------------------------------------------------------------------------
/stories/Avatar.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { Avatar } from '../src/components'
5 |
6 | Avatar.displayName = 'Avatar'
7 |
8 | storiesOf('Avatar', module)
9 | .add('without image', () => (
10 |
11 | ))
12 | .add('with an image', () => (
13 |
14 | ))
15 | .add('with upload', () => (
16 |
17 | ))
18 | .add('with custom size', () => (
19 |
20 | ))
21 | .add('default avatars based on username', () => (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ))
31 |
--------------------------------------------------------------------------------
/stories/TextField.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 | import { Eye } from 'react-feather'
5 |
6 | import { TextField, Typography } from '../src/components'
7 |
8 | TextField.displayName = 'TextField'
9 |
10 | storiesOf('TextField', module)
11 | .addParameters({
12 | info: 'A TextField with a label, which can be either idle, focused, or in an error state.'
13 | })
14 | .add('default', () => (
15 |
19 | ))
20 | .add('error', () => (
21 |
26 | ))
27 | .add('password field', () => (
28 | }
33 | onChange={action('onChange')}
34 | />
35 | ))
36 | .add('with a button', () => (
37 |
43 | ))
44 |
--------------------------------------------------------------------------------
/src/assets/background.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/stories/Button.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 |
5 | import { Button } from '../src/components'
6 |
7 | Button.displayName = 'Button'
8 |
9 | storiesOf('Button', module)
10 | .addParameters({
11 | info: 'A button of type primary (default) or secondary.'
12 | })
13 | .add('primary', () => (
14 |
18 | Primary
19 |
20 | ))
21 | .add('success', () => (
22 |
26 | Success
27 |
28 | ))
29 | .add('danger', () => (
30 |
34 | Danger
35 |
36 | ))
37 | .add('neutral', () => (
38 |
42 | Neutral
43 |
44 | ))
45 | .add('secondary', () => (
46 |
50 | Secondary
51 |
52 | ))
53 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMin.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Sidebar {
4 | height: 100vh;
5 | width: 100px;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | background-color: $color-dark-blue-100;
10 | justify-content: space-between;
11 |
12 | .header {
13 | width: 100px;
14 | height: 212px;
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | align-items: center;
19 | background-color: $color-dark-blue-200;
20 | }
21 |
22 | .nav {
23 | padding-top: 32px;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | justify-items: center;
28 | flex-direction: column;
29 | >:not(:last-child) {
30 | margin-bottom: 40px;
31 | }
32 | }
33 |
34 | .navElement {
35 | cursor: pointer;
36 | z-index: 1;
37 | &:hover * {
38 | opacity: 1;
39 | }
40 | text-decoration: none;
41 | color: white;
42 | }
43 |
44 | .active * {
45 | opacity: 1;
46 | color: $color-blue;
47 | }
48 |
49 | .logout {
50 | z-index: 1;
51 | }
52 |
53 | .cubes {
54 | position: absolute;
55 | bottom: 0;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/assets/back-to-maps.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/modals/PersistentStorageRequest/PersistentStorageRequest.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { useTranslation } from 'react-i18next'
5 | import { Button, Modal, Typography, Space } from '../../components'
6 | import styles from './PersistentStorageRequest.module.scss'
7 |
8 | const PermanentStorageRequest = ({ onClose }) => {
9 | const { t } = useTranslation()
10 | return (
11 |
12 |
13 |
14 | {t('Storage authorization')} />
15 |
16 |
17 | {`${t('In order to store your data securely,')}
18 | ${t('please authorize Masq to use the persistent storage of the browser.')}
19 | ${t('This notification will appear again if necessary.')}`}
20 |
21 |
22 |
23 |
{t('Ok')}
24 |
25 |
26 | )
27 | }
28 |
29 | PermanentStorageRequest.propTypes = {
30 | onClose: PropTypes.func.isRequired
31 | }
32 |
33 | export default PermanentStorageRequest
34 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .Modal {
5 | @media (max-width: $mobile-width) {
6 | position: relative;
7 | }
8 |
9 | .overlay {
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 100%;
15 | background-image: linear-gradient(to bottom, #6c7694, #353c52);
16 | z-index: 2;
17 | }
18 |
19 | .modal {
20 | position: fixed;
21 | top: 0;
22 | left: 0;
23 | z-index: 2;
24 | background-color: white;
25 | box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.16);
26 | opacity: 1;
27 | max-width: calc(100vw - 56px);
28 | padding-left: 28px;
29 | padding-right: 28px;
30 | padding-top: 64px;
31 | padding-bottom: 32px;
32 |
33 | @media (min-width: $mobile-width + 1px) {
34 | border-radius: 6px;
35 | top: 50%;
36 | left: 50%;
37 | transform: translate(-50%, -50%);
38 | }
39 |
40 | @media (max-width: $mobile-width) {
41 | height: calc(100% - 96px);
42 | overflow-y: auto;
43 | }
44 | }
45 |
46 | .close {
47 | position: absolute;
48 | top: 20px;
49 | right: 20px;
50 | color: #191919;
51 | opacity: 0.4;
52 | cursor: pointer;
53 | margin-bottom: 44px;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/modals/UnsupportedBrowser/UnsupportedBrowser.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useTranslation } from 'react-i18next'
3 | import { Modal, Typography, Space } from '../../components'
4 | import { SUPPORTED_BROWSERS } from '../../lib/browser'
5 | import { capitalize } from '../../lib/utils'
6 | import { AlertOctagon } from 'react-feather'
7 |
8 | import styles from './UnsupportedBrowser.module.scss'
9 |
10 | const UnsupportedBrowser = () => {
11 | const { t } = useTranslation()
12 |
13 | return (
14 |
15 |
16 |
{t('Browser not supported')}
17 |
18 |
19 |
20 |
{t('Your browser is not compatible with Masq. Please, try Masq with one of the compatible browsers:')}
21 |
22 |
23 | {SUPPORTED_BROWSERS.map((browser, index) => (
24 | - {capitalize(browser)}
25 | ))}
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default UnsupportedBrowser
33 |
--------------------------------------------------------------------------------
/src/components/Typography/Typography.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import cx from 'classnames'
4 |
5 | import styles from './Typography.module.scss'
6 |
7 | const Typography = ({ className, maxWidth, type, children, color, align, line }) => (
8 |
21 | {children}
22 |
23 | )
24 |
25 | Typography.propTypes = {
26 | className: PropTypes.string,
27 | maxWidth: PropTypes.number,
28 | children: PropTypes.any.isRequired,
29 | color: PropTypes.string,
30 | type: PropTypes.oneOf([
31 | 'title',
32 | 'title-landing',
33 | 'title-landing2',
34 | 'title-modal',
35 | 'title-page',
36 | 'subtitle-page',
37 | 'title-card',
38 | 'paragraph',
39 | 'paragraph-landing',
40 | 'paragraph-landing-dark',
41 | 'paragraph-modal',
42 | 'username',
43 | 'username-alt',
44 | 'label',
45 | 'label-nav',
46 | 'footer',
47 | 'textFieldButton',
48 | 'avatarUsername'
49 | ]).isRequired,
50 | align: PropTypes.string,
51 | line: PropTypes.bool
52 | }
53 |
54 | export default Typography
55 |
--------------------------------------------------------------------------------
/src/assets/cubes-sidebar-min.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/containers/Applications/Applications.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .Apps {
5 | flex: 1;
6 | margin: 48px 48px 48px;
7 |
8 | @media (max-width: $mobile-width) {
9 | margin: 16px;
10 | width: unset;
11 | }
12 |
13 | .topSection {
14 | display: flex;
15 | // height: 50px;
16 | justify-content: space-between;
17 | }
18 |
19 | .cards {
20 | display: grid;
21 | grid-gap: 28px;
22 | @media (min-width: 1200px) {
23 | margin-right: 365px;
24 | }
25 |
26 | @media (min-width: 1400px) {
27 | grid-template-columns: repeat(2, 1fr);
28 | }
29 | }
30 |
31 | .Card {
32 | .trashIcon {
33 | cursor: pointer;
34 | color: $color-grey-300;
35 | &:hover {
36 | color: $color-blue-grey;
37 | }
38 | }
39 | }
40 |
41 | .recommendedApps {
42 | display: flex;
43 | flex-direction: column;
44 | width: 180px;
45 | align-items: center;
46 |
47 | .icon {
48 | cursor: pointer;
49 | @media (max-width: $mobile-width) {
50 | width: 100px;
51 | height: 100px;
52 | }
53 | }
54 | }
55 | }
56 |
57 | .Link {
58 | display: flex;
59 | align-items: center;
60 | :first-child {
61 | margin-right: 8px;
62 | }
63 |
64 | a {
65 | color: inherit;
66 | text-decoration: inherit;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/assets/windows-masq.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMax.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Sidebar {
4 | height: 100vh;
5 | width: 252px;
6 | min-width: 252px;
7 | background-color: $color-dark-blue-100;
8 |
9 | .header {
10 | height: 190px;
11 | display: flex;
12 | width: 100%;
13 | flex-direction: column;
14 | justify-content: center;
15 | align-items: center;
16 | background-color: $color-dark-blue-200;
17 | }
18 |
19 | .nav {
20 | padding-top: 42px;
21 | display: flex;
22 | flex-direction: column;
23 | >:not(:last-child) {
24 | margin-bottom: 40px;
25 | }
26 | }
27 |
28 | .navElement {
29 | display: flex;
30 | margin-left: 70px;
31 | align-items: center;
32 | cursor: pointer;
33 | opacity: 0.8;
34 | z-index: 1;
35 | &:hover {
36 | opacity: 1;
37 | }
38 | :first-child {
39 | margin-right: 12px;
40 | }
41 | text-decoration: none;
42 | color: white;
43 | }
44 |
45 | .active * {
46 | color: $color-blue;
47 | }
48 |
49 | .active.navElement {
50 | opacity: 1;
51 | &::before {
52 | position: absolute;
53 | content: '';
54 | left: 44px;
55 | height: 21px;
56 | width: 2px;
57 | border-radius: 1px;
58 | background-color: $color-blue;
59 | }
60 | }
61 |
62 | .logout {
63 | z-index: 1;
64 | position: absolute;
65 | bottom: 64px;
66 | }
67 |
68 | .cubes {
69 | position: absolute;
70 | bottom: 0;
71 | z-index: 0;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/containers/Profile/Profile.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .Profile {
5 | width: 100%;
6 | justify-content: space-between;
7 | margin: 48px 48px 16px 48px;
8 | // height: 100%;
9 |
10 | @media (max-width: $mobile-width) {
11 | margin: 16px;
12 | width: unset;
13 | }
14 |
15 | .titleContainer {
16 | margin-bottom: 32px;
17 | }
18 |
19 | .page {
20 | display: flex;
21 | height: 100%;
22 | justify-content: space-between;
23 |
24 | @media (max-width: 700px) {
25 | flex-direction: column;
26 | align-items: center;
27 | justify-content: unset;
28 | }
29 |
30 | .content {
31 | display: flex;
32 | flex-direction: row;
33 |
34 | .inputs {
35 | height: 264px;
36 | margin-right: 32px;
37 | margin-left: 64px;
38 | display: flex;
39 | flex-direction: column;
40 | justify-content: space-between;
41 | }
42 |
43 | @media (max-width: 1024px) {
44 | flex-direction: column;
45 | align-items: center;
46 |
47 | .inputs {
48 | margin-top: 24px;
49 | margin-left: 0;
50 | margin-right: 0;
51 | margin-bottom: 32px;
52 | }
53 | }
54 | }
55 |
56 | .rightSection {
57 | .deleteButton {
58 | cursor: pointer;
59 | .trashIcon {
60 | vertical-align: text-bottom;
61 | margin-right: 4px;
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/src/containers/Login/Login.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Login {
4 | background-color: $color-dark-blue-100;
5 | position: relative;
6 | min-height: 100vh;
7 | display: flex;
8 | justify-content: space-between;
9 | flex-direction: column;
10 | align-items: center;
11 | text-align: center;
12 |
13 | .content {
14 | width: 100%;
15 | }
16 |
17 | .Background {
18 | background-color: $color-dark-blue-100;
19 | flex-shrink: 0;
20 | }
21 | }
22 |
23 | .usersSelection {
24 | width: 100%;
25 | text-align: center;
26 | }
27 |
28 | .users {
29 | width: 100%;
30 | display: grid;
31 | grid-template-columns: repeat(auto-fit, minmax(120px, 120px));
32 | grid-gap: 64px;
33 | text-align: center;
34 | justify-content: center;
35 |
36 | .PlusSquare {
37 | margin-right: auto;
38 | margin-left: auto;
39 | cursor: pointer;
40 | opacity: 0.5;
41 | &:hover {
42 | opacity: 1;
43 | }
44 | }
45 |
46 | .user {
47 | cursor: pointer;
48 | text-decoration: none;
49 | }
50 | }
51 |
52 | .userPassword {
53 | text-align: center;
54 | margin: auto;
55 | width: 320px;
56 | .passwordSection {
57 | display: flex;
58 | flex-direction: column;
59 | align-items: center;
60 | }
61 | }
62 |
63 | .goback {
64 | text-decoration: none;
65 | display: grid;
66 | color: white;
67 | grid-column-gap: 8px;
68 | align-items: center;
69 | justify-content: center;
70 | grid-template-columns: auto auto;
71 | margin-bottom: 16px;
72 | cursor: pointer;
73 | }
74 |
--------------------------------------------------------------------------------
/src/modals/DeleteProfileDialog/DeleteProfileDialog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { AlertCircle } from 'react-feather'
4 | import { useTranslation } from 'react-i18next'
5 | import { Button, Modal, Typography, Space } from '../../components'
6 |
7 | import styles from './DeleteProfileDialog.module.scss'
8 |
9 | const ConfirmDialog = ({ username, onConfirm, onCancel, onClose }) => {
10 | const { t } = useTranslation()
11 | return (
12 |
13 |
14 |
{t('Deletion of the profile')}
15 |
16 |
17 |
18 |
19 | {`${t('You are at the point of deleting the profile ')} « ${username} ».
20 | ${t(' All your personal data of Masq will be lost.')}`}
21 |
22 |
23 |
24 | {t('Cancel')}
25 | {t('Delete the profile')}
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | ConfirmDialog.propTypes = {
33 | username: PropTypes.string.isRequired,
34 | onConfirm: PropTypes.func.isRequired,
35 | onCancel: PropTypes.func.isRequired,
36 | onClose: PropTypes.func.isRequired
37 | }
38 |
39 | export default ConfirmDialog
40 |
--------------------------------------------------------------------------------
/src/components/Button/Button.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/mixins.scss';
2 | @import '../../styles/colors.scss';
3 | @import '../../styles/variables.scss';
4 |
5 | .Button {
6 | height: 50px;
7 | padding: 0;
8 | outline: none;
9 | border: none;
10 | cursor: pointer;
11 | transition: box-shadow 0.5s;
12 |
13 | @media (max-width: $mobile-width) {
14 | &:not(.secondary) {
15 | width: 320px !important;
16 | max-width: 100% !important;
17 | }
18 | }
19 |
20 | span {
21 | text-transform: uppercase;
22 | font-size: 12px;
23 | font-weight: 600;
24 | line-height: 2;
25 | letter-spacing: 0.5px;
26 | text-align: center;
27 | color: white;
28 | }
29 | }
30 |
31 | .secondary {
32 | opacity: 0.8;
33 | border: solid 1px $color-grey-300;
34 | background-color: $color-grey-100;
35 |
36 | span {
37 | color: $color-grey-300;
38 | }
39 |
40 | &:hover {
41 | border: solid 1px $color-blue-grey;
42 | span {
43 | color: $color-blue-grey;
44 | }
45 | }
46 | }
47 |
48 | .primary {
49 | background-color: $color-blue;
50 | &:hover {
51 | box-shadow: 0 5px 16px 0 rgba(69, 179, 248, 0.4);
52 | }
53 | }
54 |
55 | .success {
56 | background-color: $color-green;
57 | &:hover {
58 | box-shadow: 0 5px 16px 0 rgba(64, 174, 108, 0.4);
59 | }
60 | }
61 |
62 | .danger {
63 | background-color: $color-red;
64 | &:hover {
65 | box-shadow: 0 5px 16px 0 rgba(229, 59, 91, 0.4);
66 | }
67 | }
68 |
69 | .neutral {
70 | background-color: $color-blue-grey;
71 | &:hover {
72 | box-shadow: 0 5px 16px 0 rgba(92, 111, 132, 0.4)
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/Card/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Image from './Image'
5 | import { Typography } from '..'
6 |
7 | import styles from './Card.module.scss'
8 | import Space from '../Space'
9 |
10 | const Description = ({ description }) => (
11 |
12 | {description}
13 |
14 | )
15 |
16 | Description.propTypes = {
17 | description: PropTypes.string.isRequired
18 | }
19 |
20 | const Card = ({ minHeight, image, title, actions, description, footer, color, width }) => (
21 |
22 | {image &&
}
23 |
24 | {(actions || color) && (
25 |
29 | )}
30 | {!color && !actions &&
}
31 | {title &&
{title} }
32 | {description &&
}
33 | {footer}
34 |
35 |
36 | )
37 |
38 | Card.defaultProps = {
39 | minHeight: 50
40 | }
41 |
42 | Card.propTypes = {
43 | width: PropTypes.number,
44 | minHeight: PropTypes.number,
45 | image: PropTypes.string,
46 | title: PropTypes.string,
47 | actions: PropTypes.object,
48 | description: PropTypes.string,
49 | footer: PropTypes.object,
50 | color: PropTypes.string
51 | }
52 |
53 | export default Card
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Masq-app
2 |
3 | [](https://standardjs.com)
4 |
5 | `AKASHA.id` is the Web application that offers a user-centric identity management. This application was based on an intitial implementation called [Masq](https://github.com/QwantResearch/masq-app).
6 |
7 | ## Development
8 |
9 | ```bash
10 | npm install
11 | npm start
12 | ```
13 |
14 | ## Deploy to Github pages
15 |
16 | ```bash
17 | npm install
18 | npm run deploy
19 | ```
20 |
21 | ## Production
22 |
23 | To deploy in production:
24 |
25 | - Edit `.env.production` to point to the correct signalhubws server(s)
26 | - Set the `homepage` field in `package.json` to point to the url where masq-app will be accessible, then run:
27 |
28 | ```bash
29 | npm install
30 | npm run build
31 | ```
32 |
33 | Then deploy the static files in the `build/` folder.
34 |
35 | ## Environment variables
36 |
37 | For now we have to specify an env variable with the urls of the signalhubs (signalling server) that will be used.
38 | `
39 | REACT_APP_SIGNALHUB_URLS
40 | `
41 |
42 | # Remote webrtc
43 |
44 | For Masq app to display a button showing the remote connection link and QR code and possibly use a STUN or TURN server, you will need to specify the following env variable.
45 |
46 | `
47 | REACT_APP_REMOTE_WEBRTC
48 | `
49 |
50 | To be able to connect between different devices, a STUN and TURN server might be needed, ou can set stun and turn servers with the following env variables. Multiple URLs can be specified as a single comma separated string.
51 |
52 | `
53 | REACT_APP_STUN_URLS
54 | REACT_APP_TURN_URLS
55 | `
56 |
--------------------------------------------------------------------------------
/src/components/TextField/TextField.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 | @import '../../styles/variables.scss';
3 |
4 | .TextField {
5 | position: relative;
6 | width: 302px;
7 | display: flex;
8 | flex-direction: column-reverse;
9 |
10 | @media (max-width: $mobile-width) {
11 | width: 320px;
12 | }
13 |
14 | label {
15 | font-size: 12px;
16 | line-height: 1.25;
17 | &:first-letter {
18 | text-transform: capitalize;
19 | }
20 | }
21 |
22 | input {
23 | height: 29px;
24 | border-radius: 3px;
25 | outline: none;
26 | padding-left: 16px;
27 | padding-right: 16px;
28 | }
29 |
30 | .button {
31 | position: absolute;
32 | right: 14px;
33 | display: flex;
34 | flex-direction: column;
35 | justify-content: center;
36 | height: 32px;
37 | cursor: pointer;
38 | color: $color-dark-blue-100;
39 | }
40 | }
41 |
42 | .large {
43 | input {
44 | height: 46px;
45 | font-size: 20px;
46 | color: $color-dark-blue-100;
47 | }
48 |
49 | .button {
50 | height: 50px;
51 | }
52 | }
53 |
54 | .default {
55 | label {
56 | color: $color-blue-grey;
57 | }
58 |
59 | input {
60 | border: solid 1px $color-grey-300;
61 | &:focus {
62 | border: solid 1px $color-blue;
63 | + label {
64 | color: $color-blue;
65 | }
66 | }
67 | }
68 | }
69 |
70 | .error {
71 | label {
72 | color: $color-red;
73 | }
74 |
75 | input {
76 | border: solid 1px $color-red;
77 | &:focus {
78 | border: solid 1px $color-red;
79 | }
80 | }
81 | }
82 |
83 | .password {
84 | input {
85 | padding-right: 48px;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/modals/DeleteAppDialog/DeleteAppDialog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Button, Modal, Typography, Space, Card } from '../../components'
5 | import { useTranslation } from 'react-i18next'
6 | import styles from './DeleteAppDialog.module.scss'
7 |
8 | const ConfirmDialog = ({ app, onConfirm, onCancel, onClose }) => {
9 | const { t } = useTranslation()
10 | return (
11 |
12 |
13 |
{`${t('Deletion of the application')} ${app.appId}`}
14 |
15 |
16 | {`${t('You are at the point of deleting the application ')} "${app.appId}" ${t('from Masq with all the associated data.')}
17 | ${t(' All the data will be lost.')}`}
18 |
19 |
20 |
24 |
25 |
26 | {t('Cancel')}
27 | {t('Delete the application')}
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | ConfirmDialog.propTypes = {
35 | app: PropTypes.object.isRequired,
36 | onConfirm: PropTypes.func.isRequired,
37 | onCancel: PropTypes.func.isRequired,
38 | onClose: PropTypes.func.isRequired
39 | }
40 |
41 | export default ConfirmDialog
42 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import PropTypes from 'prop-types'
3 | import MediaQuery from 'react-responsive'
4 |
5 | import { X } from 'react-feather'
6 |
7 | import styles from './Modal.module.scss'
8 |
9 | const Modal = ({ onClose, width, padding, children }) => (
10 |
11 |
12 |
19 | {onClose && }
20 | {children}
21 |
22 |
23 | )
24 |
25 | const ResponsiveModal = ({ onClose, width, children, padding }) => {
26 | useEffect(() => {
27 | window.document.body.style.overflow = 'hidden'
28 |
29 | // cleanup
30 | return () => {
31 | window.document.body.style.overflow = 'unset'
32 | }
33 | }, [])
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | Modal.propTypes =
48 | ResponsiveModal.propTypes = {
49 | onClose: PropTypes.func,
50 | children: PropTypes.object,
51 | width: PropTypes.oneOfType([
52 | PropTypes.string,
53 | PropTypes.number
54 | ]),
55 | padding: PropTypes.oneOfType([
56 | PropTypes.string,
57 | PropTypes.number
58 | ])
59 | }
60 |
61 | export default ResponsiveModal
62 |
--------------------------------------------------------------------------------
/stories/Card.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 |
4 | import { Card, Switch, Typography } from '../src/components'
5 |
6 | const Actions = () => (
7 |
11 | )
12 |
13 | const Footer = () => (
14 | Footer
15 | )
16 |
17 | Actions.displayName = 'Actions'
18 | Footer.displayName = 'Footer'
19 |
20 | storiesOf('Card', module)
21 | .addParameters({
22 | info: 'A Card can contain two children: actions displayed in the top right corner, and a footer.'
23 | })
24 | .add('with title and description', () => (
25 |
30 | ))
31 | .add('with image', () => (
32 | }
38 | />
39 | ))
40 | .add('with actions and a footer', () => (
41 | }
47 | footer={}
48 | />
49 | ))
50 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | AKASHA ID
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Avatar {
4 | position: relative;
5 | border-radius: 10px;
6 |
7 | .avatarUsername {
8 | width: 100%;
9 | height: 100%;
10 | border-radius: 10px;
11 | display: flex;
12 | flex-direction: column;
13 | justify-content: center;
14 | align-items: center;
15 | }
16 |
17 | .camera {
18 | position: absolute;
19 | color: $color-grey-300;
20 | visibility: hidden;
21 | left: 50%;
22 | top: 50%;
23 | transform: translate(-50%, -50%);
24 | }
25 |
26 | .img {
27 | width: 100%;
28 | height: 100%;
29 | border-radius: 10px;
30 | background-size: cover;
31 | background-position: top center;
32 | }
33 | }
34 |
35 | .new {
36 | position: relative;
37 | box-shadow:0 0 0 2px $color-grey-300;
38 | transition: all 0.3s ease-in-out;
39 | cursor: pointer;
40 |
41 | .camera {
42 | visibility: visible;
43 | opacity: 1;
44 | transition: all 0.3s ease-in-out;
45 | }
46 |
47 | &:hover {
48 | box-shadow:0 0 0 2px $color-grey-300;
49 | .camera {
50 | color: $color-grey-300;
51 | }
52 | }
53 | }
54 |
55 | .upload {
56 | position: relative;
57 | transition: all 0.3s ease-in-out;
58 | cursor: pointer;
59 |
60 | .camera {
61 | transition: visibility 0s, opacity 0.3s linear;
62 | visibility: visible;
63 | opacity: 0;
64 | }
65 |
66 | &:hover {
67 | filter: grayscale(50%);
68 | cursor: pointer;
69 |
70 | .camera {
71 | visibility: visible;
72 | opacity: 1;
73 | }
74 | }
75 | }
76 |
77 | .mobile {
78 | .img {
79 | border-radius: 50px;
80 | }
81 |
82 | .avatarUsername {
83 | border-radius: 50px;
84 | }
85 |
86 | p {
87 | font-size: 24px;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMobile.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .NavbarMobile {
4 | width: 100%;
5 |
6 | .header {
7 | height: 64px;
8 | background-color: $color-dark-blue-200;
9 | .content {
10 | display: flex;
11 | height: 100%;
12 | margin-left: 16px;
13 | margin-right: 16px;
14 | align-items: center;
15 | justify-content: space-between;
16 |
17 | .user {
18 | display: flex;
19 | align-items: center;
20 | cursor: pointer;
21 |
22 | img {
23 | width: 32px;
24 | height: 32px;
25 | border-radius: 50px;
26 | }
27 |
28 | .chevron {
29 | margin-left: 12px;
30 | }
31 | }
32 | }
33 | }
34 |
35 | .nav {
36 | width: 100%;
37 | height: 56px;
38 | background-color: $color-dark-blue-100;
39 | color: white;
40 |
41 | .content {
42 | display: flex;
43 | height: 100%;
44 | align-items: center;
45 | justify-content: space-between;
46 | margin-left: 16px;
47 | margin-right: 16px;
48 |
49 | .navElement {
50 | position: relative;
51 | display: flex;
52 | height: 100%;
53 | align-items: center;
54 | cursor: pointer;
55 | text-decoration: none;
56 | color: white;
57 | opacity: 0.8;
58 | &:hover {
59 | opacity: 1;
60 | }
61 | :first-child {
62 | margin-right: 12px;
63 | }
64 |
65 | }
66 |
67 | .active * {
68 | color: $color-blue;
69 | opacity: 1;
70 |
71 | &:before {
72 | position: absolute;
73 | content: '';
74 | left: 0;
75 | bottom: 0;
76 | height: 2px;
77 | width: 100%;
78 | border-radius: 1px;
79 | background-color: $color-blue;
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.module.scss:
--------------------------------------------------------------------------------
1 | /* Switch dimensions */
2 | $width: 50px;
3 | $height: 24px;
4 | $width-secondary: 32px;
5 | $height-secondary: 8px;
6 |
7 | /* slider dimensions */
8 | $slider-size: 20px;
9 | $slider-size-secondary: 16px;
10 | $slider-radius: 12px;
11 |
12 | $slider-top: 2px;
13 | $slider-left: 4px;
14 |
15 | $transition: 0.3s;
16 |
17 | .Switch {
18 | display: flex;
19 | align-items: center;
20 | :first-child {
21 | margin-right: 8px;
22 | }
23 | }
24 |
25 | .SwitchButton {
26 | position: relative;
27 | display: inline-block;
28 | width: $width;
29 | height: $height;
30 |
31 | input {
32 | display: none;
33 |
34 | &:checked {
35 | + .slider {
36 | &:before {
37 | transform: translateX(#{$width - $slider-size - $slider-left * 2});
38 | }
39 | }
40 | }
41 | }
42 |
43 | .slider {
44 | position: absolute;
45 | cursor: pointer;
46 | width: 100%;
47 | height: 100%;
48 | transition: $transition;
49 |
50 | border-radius: $slider-radius;
51 |
52 | &:before {
53 | border-radius: $slider-radius;
54 | position: absolute;
55 | content: '';
56 | top: $slider-top;
57 | left: $slider-left;
58 | width: $slider-size;
59 | height: $slider-size;
60 | background-color: white;
61 | transition: $transition;
62 | box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.16);
63 | }
64 | }
65 | }
66 |
67 | .secondary {
68 | height: $height-secondary;
69 | width: $width-secondary;
70 |
71 | input {
72 | &:checked {
73 | + .slider {
74 | &:before {
75 | transform: translateX(#{$width-secondary - $slider-size-secondary});
76 | }
77 | }
78 | }
79 | }
80 |
81 | .slider {
82 | &:before {
83 | width: $slider-size-secondary;
84 | height: $slider-size-secondary;
85 | top: - ($slider-size-secondary - $height-secondary) / 2;
86 | left: 0;
87 | box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.25);
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/stories/Switch.stories.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { action } from '@storybook/addon-actions'
4 | import PropTypes from 'prop-types'
5 |
6 | import { Switch } from '../src/components/'
7 |
8 | Switch.displayName = 'Switch'
9 |
10 | const COLOR_OFF = '#c8cbd3'
11 | const COLOR_ON = '#40ae6c'
12 |
13 | class SwitchExample extends Component {
14 | constructor (props) {
15 | super(props)
16 | this.state = {
17 | checked: false
18 | }
19 | this.getColor = this.getColor.bind(this)
20 | this.handleChange = this.handleChange.bind(this)
21 | }
22 |
23 | getColor () {
24 | return this.state.checked ? COLOR_ON : COLOR_OFF
25 | }
26 |
27 | handleChange (e) {
28 | this.setState({
29 | checked: e.target.checked
30 | })
31 | }
32 |
33 | render () {
34 | return (
35 |
41 | )
42 | }
43 | }
44 |
45 | SwitchExample.propTypes = {
46 | secondary: PropTypes.bool
47 | }
48 |
49 | storiesOf('Switch', module)
50 | .addParameters({
51 | info: 'Switch component of types primary or secondary, to toggle on/off.'
52 | })
53 | .add('on', () => (
54 |
55 | ))
56 | .add('off', () => (
57 |
58 | ))
59 | .add('without label', () => (
60 |
61 | ))
62 | .add('secondary on', () => (
63 |
64 | ))
65 | .add('secondary off', () => (
66 |
67 | ))
68 | .add('on / off (example)', () => (
69 |
70 | ))
71 | .add('on / off secondary (example)', () => (
72 |
73 | ))
74 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 | import { Camera } from 'react-feather'
5 |
6 | import { Typography } from '../'
7 |
8 | import styles from './Avatar.module.scss'
9 |
10 | const COLORS = [
11 | styles.colorCyan,
12 | styles.colorBlue,
13 | styles.colorGreen,
14 | styles.colorRed,
15 | styles.colorBlueGrey,
16 | styles.colorYellow
17 | ]
18 |
19 | export default class Avatar extends React.Component {
20 | constructor (props) {
21 | super(props)
22 | this.openDialog = this.openDialog.bind(this)
23 | }
24 |
25 | openDialog () {
26 | this.refs.fileDialog.click()
27 | }
28 |
29 | render () {
30 | const { size, image, upload, onChange, username } = this.props
31 | const style = { backgroundImage: 'url(' + image + ')' }
32 |
33 | return (
34 |
44 |
48 |
49 |
50 |
51 | {!image && username && (
52 |
56 | {username[0].toUpperCase()}
57 |
58 | )}
59 |
60 | {image &&
}
61 |
62 | )
63 | }
64 | }
65 |
66 | Avatar.defaultProps = {
67 | size: 120
68 | }
69 |
70 | Avatar.propTypes = {
71 | size: PropTypes.number,
72 | upload: PropTypes.bool,
73 | image: PropTypes.string,
74 | onChange: PropTypes.func,
75 | username: PropTypes.string
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/cubes-sidebar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/containers/Devices/Devices.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { Redirect } from 'react-router-dom'
5 | import { Card, Button, Typography, Space } from '../../components'
6 | import { withTranslation } from 'react-i18next'
7 | import { SyncDevice } from '../../modals'
8 |
9 | import styles from './Devices.module.scss'
10 |
11 | class Devices extends React.Component {
12 | constructor (props) {
13 | super(props)
14 | this.state = { addDevice: false }
15 | this.handleAddDeviceClick = this.handleAddDeviceClick.bind(this)
16 | this.renderSyncModal = this.renderSyncModal.bind(this)
17 | this.handleSyncModalClosed = this.handleSyncModalClosed.bind(this)
18 | }
19 |
20 | handleAddDeviceClick () {
21 | this.setState({ addDevice: true })
22 | }
23 |
24 | handleSyncModalClosed () {
25 | this.setState({ addDevice: false })
26 | }
27 |
28 | renderSyncModal () {
29 | return this.state.addDevice ? : false
30 | }
31 |
32 | render () {
33 | const { user, devices, t } = this.props
34 |
35 | if (!user) return
36 |
37 | return (
38 |
39 | {this.renderSyncModal()}
40 |
41 | {t('My devices')}
42 | {t('Add a new devce (coming soon)')}
43 |
44 |
45 |
46 |
47 | {devices.map((device, index) => (
48 |
49 |
50 |
51 | ))}
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | const mapStateToProps = (state) => ({
59 | user: state.masq.currentUser,
60 | devices: state.masq.devices
61 | })
62 |
63 | Devices.propTypes = {
64 | user: PropTypes.object,
65 | devices: PropTypes.arrayOf(PropTypes.object),
66 | t: PropTypes.func
67 | }
68 | const translatedDevices = withTranslation()(Devices)
69 | export default connect(mapStateToProps)(translatedDevices)
70 |
--------------------------------------------------------------------------------
/src/assets/shield.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMin.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { Redirect, NavLink } from 'react-router-dom'
4 | import PropTypes from 'prop-types'
5 | import { Grid, Smartphone, User, LogOut } from 'react-feather'
6 |
7 | import { Avatar, Typography, Space } from '../../components'
8 | import { signout } from '../../actions'
9 | import { ReactComponent as Cubes } from '../../assets/cubes-sidebar-min.svg'
10 |
11 | import styles from './NavbarMin.module.scss'
12 |
13 | class NavbarMin extends React.Component {
14 | render () {
15 | const { user, signout } = this.props
16 |
17 | if (!user) return
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
{user.username}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
58 | NavbarMin.propTypes = {
59 | user: PropTypes.object,
60 | signout: PropTypes.func
61 | }
62 |
63 | const mapStateToProps = (state) => ({
64 | user: state.masq.currentUser
65 | })
66 |
67 | const mapDispatchToProps = dispatch => ({
68 | signout: () => dispatch(signout())
69 | })
70 |
71 | export default connect(mapStateToProps, mapDispatchToProps)(NavbarMin)
72 |
--------------------------------------------------------------------------------
/src/assets/cubes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMax.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { Redirect, NavLink } from 'react-router-dom'
4 | import PropTypes from 'prop-types'
5 | import { Grid, Smartphone, User, LogOut } from 'react-feather'
6 | import { withTranslation } from 'react-i18next'
7 |
8 | import { Avatar, Space, Typography } from '../../components'
9 | import { signout } from '../../actions'
10 | import { ReactComponent as Cubes } from '../../assets/cubes-sidebar.svg'
11 |
12 | import styles from './NavbarMax.module.scss'
13 |
14 | class NavbarMax extends React.Component {
15 | render () {
16 | const { user, signout, t } = this.props
17 |
18 | if (!user) return
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
{user.username}
27 |
28 |
29 |
30 |
31 | {t('Applications')}
32 |
33 |
34 |
35 | {t('Devices')}
36 |
37 |
38 |
39 | {t('Profile')}
40 |
41 |
42 |
43 |
44 |
45 | {t('Sign out')}
46 |
47 |
48 |
49 |
50 | )
51 | }
52 | }
53 |
54 | NavbarMax.propTypes = {
55 | user: PropTypes.object,
56 | signout: PropTypes.func,
57 | t: PropTypes.func
58 | }
59 |
60 | const mapStateToProps = (state) => ({
61 | user: state.masq.currentUser
62 | })
63 |
64 | const mapDispatchToProps = dispatch => ({
65 | signout: () => dispatch(signout())
66 | })
67 |
68 | const translatedNavbarMax = withTranslation()(NavbarMax)
69 | export default connect(mapStateToProps, mapDispatchToProps)(translatedNavbarMax)
70 |
--------------------------------------------------------------------------------
/src/components/TextField/TextField.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Eye, EyeOff } from 'react-feather'
4 |
5 | import cx from 'classnames'
6 |
7 | import { Typography } from '..'
8 | import styles from './TextField.module.scss'
9 |
10 | const TextField = ({ className, label, error, type, onChange, onKeyUp, autoFocus, placeholder, defaultValue, large, button, onClick, password, readOnly, height }) => {
11 | const [visible, setVisible] = useState(false)
12 |
13 | const handleClick = () => {
14 | if (password) {
15 | setVisible(!visible)
16 | }
17 | if (onClick) {
18 | onClick()
19 | }
20 | }
21 |
22 | return (
23 |
32 |
42 |
{label}
43 |
44 | {(password || button) && (
45 |
46 | {password && visible && defaultValue.length > 0 && }
47 | {password && !visible && defaultValue.length > 0 && }
48 | {button && {button} }
49 |
50 | )}
51 |
52 | )
53 | }
54 |
55 | TextField.defaultProps = {
56 | error: false,
57 | focus: false,
58 | label: '',
59 | type: 'text',
60 | defaultValue: ''
61 | }
62 |
63 | TextField.propTypes = {
64 | large: PropTypes.bool,
65 | className: PropTypes.string,
66 |
67 | /** The label of the text field */
68 | label: PropTypes.string,
69 |
70 | /** Set to true if the value is incorrect */
71 | error: PropTypes.bool,
72 |
73 | /** Onput type */
74 | type: PropTypes.oneOf(['text', 'password']),
75 |
76 | /** onChange */
77 | onChange: PropTypes.func,
78 |
79 | onKeyUp: PropTypes.func,
80 |
81 | autoFocus: PropTypes.bool,
82 |
83 | placeholder: PropTypes.string,
84 |
85 | defaultValue: PropTypes.string,
86 |
87 | button: PropTypes.object,
88 |
89 | onClick: PropTypes.func,
90 |
91 | password: PropTypes.bool,
92 |
93 | readOnly: PropTypes.bool,
94 |
95 | height: PropTypes.number
96 | }
97 |
98 | export default TextField
99 |
--------------------------------------------------------------------------------
/src/modals/QRCodeModal/QRCodeModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import QRCode from 'qrcode.react'
5 | import { Copy } from 'react-feather'
6 | import { useTranslation } from 'react-i18next'
7 | import classNames from 'classnames'
8 |
9 | import { Modal, Typography, Space, TextField } from '../../components'
10 |
11 | import styles from './QRCodeModal.module.scss'
12 |
13 | const Pill = ({ children }) => (
14 | {children}
15 | )
16 |
17 | Pill.propTypes = {
18 | children: PropTypes.string.isRequired
19 | }
20 |
21 | const QRCodeModal = ({ onClose, currentAppRequest }) => {
22 | const { t } = useTranslation()
23 | const [copied, setCopied] = useState(false)
24 |
25 | const copyLink = () => {
26 | const link = document.querySelector('input')
27 | link.select()
28 | document.execCommand('copy')
29 | setCopied(true)
30 | setTimeout(() => setCopied(false), 3000)
31 | }
32 |
33 | return (
34 |
35 |
36 |
{t('Add a device')}
37 |
38 |
{t('Scan this QR Code with the device you want to synchronize:')}
39 |
40 |
41 |
42 |
{t('or')}
43 |
44 |
{t('Copy the following link and paste it in the browser you want to use:')}
45 |
46 |
}
55 | height={36}
56 | onClick={() => copyLink()}
57 | />
58 |
59 | {copied &&
{t('Link Copied !')} }
60 | {!copied &&
}
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | QRCodeModal.propTypes = {
68 | onClose: PropTypes.func,
69 | currentAppRequest: PropTypes.object
70 | }
71 |
72 | const mapStateToProps = state => ({
73 | currentAppRequest: state.masq.currentAppRequest
74 | })
75 |
76 | export default connect(mapStateToProps)(QRCodeModal)
77 |
--------------------------------------------------------------------------------
/src/containers/Navbar/NavbarMobile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import { NavLink, Redirect } from 'react-router-dom'
5 | import { Grid, Smartphone, User, ChevronDown } from 'react-feather'
6 | import { withTranslation } from 'react-i18next'
7 | import { signout } from '../../actions'
8 | import { Typography, Dropdown, Avatar, Space } from '../../components'
9 |
10 | import styles from './NavbarMobile.module.scss'
11 |
12 | class NavbarMobile extends React.Component {
13 | constructor (props) {
14 | super(props)
15 | this.state = {
16 | hovered: false
17 | }
18 | this.handleClick = this.handleClick.bind(this)
19 | }
20 |
21 | handleClick () {
22 | this.setState({ hovered: !this.state.hovered })
23 | }
24 |
25 | render () {
26 | const { hovered } = this.state
27 | const { user, signout, t } = this.props
28 |
29 | if (!user) return
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
{user.username}
39 |
40 | {hovered &&
}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {t('Applications')}
49 |
50 |
51 |
52 | {t('Devices')}
53 |
54 |
55 |
56 | {t('Profile')}
57 |
58 |
59 |
60 |
61 | )
62 | }
63 | }
64 |
65 | NavbarMobile.propTypes = {
66 | user: PropTypes.object,
67 | signout: PropTypes.func,
68 | t: PropTypes.func
69 | }
70 |
71 | const mapStateToProps = (state) => ({
72 | user: state.masq.currentUser
73 | })
74 |
75 | const mapDispatchToProps = dispatch => ({
76 | signout: () => dispatch(signout())
77 | })
78 |
79 | const translatedNavbarMobile = withTranslation()(NavbarMobile)
80 | export default connect(mapStateToProps, mapDispatchToProps)(translatedNavbarMobile)
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "masq-app",
3 | "version": "0.12.0",
4 | "private": true,
5 | "license": "GPL-3.0",
6 | "scripts": {
7 | "start": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts start",
8 | "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
9 | "test": "eslint --ext .js,.jsx ./src && concurrently -k -s first \"signalhubws -p 8080\" \"karma start\"",
10 | "lint": "eslint --fix --ext .js,.jsx ./src",
11 | "scan-i18n": "i18next-scanner",
12 | "eject": "react-scripts eject",
13 | "predeploy": "npm run build",
14 | "deploy": "gh-pages -d build",
15 | "storybook": "start-storybook -p 6006",
16 | "build-storybook": "build-storybook"
17 | },
18 | "browserslist": [
19 | ">0.2%",
20 | "not dead",
21 | "not ie <= 11",
22 | "not op_mini all"
23 | ],
24 | "dependencies": {
25 | "bluebird": "^3.5.3",
26 | "classnames": "^2.2.6",
27 | "compressorjs": "^1.0.5",
28 | "detect-browser": "^3.0.1",
29 | "i18next": "^15.1.0",
30 | "i18next-browser-languagedetector": "^3.0.1",
31 | "i18next-xhr-backend": "^2.0.1",
32 | "masq-common": "git+https://github.com/QwantResearch/masq-common",
33 | "pump": "^3.0.0",
34 | "react": "^16.6.0",
35 | "react-dom": "^16.6.0",
36 | "react-feather": "^1.1.6",
37 | "react-i18next": "^10.9.0",
38 | "react-redux": "^6.0.0",
39 | "react-responsive": "^6.1.1",
40 | "react-router": "^4.3.1",
41 | "react-router-dom": "^4.3.1",
42 | "react-scripts": "^2.1.8",
43 | "redux": "^4.0.1",
44 | "redux-thunk": "^2.3.0",
45 | "signalhubws": "git+https://github.com/QwantResearch/signalhubws#masq",
46 | "typeface-asap": "0.0.35",
47 | "typeface-asap-condensed": "0.0.35",
48 | "uuid": "^3.3.2",
49 | "webrtc-swarm": "git+https://github.com/QwantResearch/webrtc-swarm#masq"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "^7.4.0",
53 | "@storybook/addon-actions": "^5.0.3",
54 | "@storybook/addon-links": "^5.0.3",
55 | "@storybook/addons": "^5.0.3",
56 | "@storybook/react": "^5.0.3",
57 | "babel-loader": "^8.0.5",
58 | "chai": "^4.2.0",
59 | "concurrently": "^4.1.0",
60 | "es6-promisify": "^6.0.1",
61 | "eslint-config-standard": "^12.0.0",
62 | "eslint-config-standard-react": "^7.0.2",
63 | "eslint-plugin-import": "^2.14.0",
64 | "eslint-plugin-mocha": "^5.3.0",
65 | "eslint-plugin-node": "^8.0.0",
66 | "eslint-plugin-promise": "^4.0.1",
67 | "eslint-plugin-react": "^7.11.1",
68 | "eslint-plugin-standard": "^4.0.0",
69 | "gh-pages": "^2.0.1",
70 | "hyperdb": "^3.5.0",
71 | "i18next-scanner": "^2.10.1",
72 | "karma": "^4.0.1",
73 | "karma-chai": "^0.1.0",
74 | "karma-chrome-launcher": "^2.2.0",
75 | "karma-mocha": "^1.3.0",
76 | "karma-sourcemap-loader": "^0.3.7",
77 | "karma-webpack": "^3.0.5",
78 | "mocha": "^6.0.2",
79 | "node-sass": "^4.9.4",
80 | "puppeteer": "^1.13.0",
81 | "qrcode.react": "^0.9.3",
82 | "random-access-memory": "^3.1.1",
83 | "sinon": "^7.3.2",
84 | "webpack-cli": "^3.2.3"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/PasswordStrength/PasswordStrength.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/mixins.scss';
2 | @import '../../styles/colors.scss';
3 | .PasswordStrength {
4 | width: 300px;
5 | .PasswordRules ul {
6 | padding: 0;
7 | margin: 0;
8 | }
9 | .PasswordRules {
10 | margin-top: 10px;
11 | display: flex;
12 | justify-content: space-between;
13 | align-items: flex-end;
14 | width: max-content;
15 | .icon {
16 | position: relative;
17 | .lockIcon {
18 | color: $color-green;
19 | position: absolute;
20 | left: 50%;
21 | top: 45%;
22 | transform: translate(-50%, -50%);
23 | }
24 | .force-0 {
25 | color: $color-red;
26 | }
27 | .force-1 {
28 | color: $color-red;
29 | }
30 | .force-2 {
31 | color: $color-yellow;
32 | }
33 | .force-3 {
34 | color: $color-yellow;
35 | }
36 | .force-4 {
37 | color: $color-yellow;
38 | }
39 | .force-5 {
40 | color: $color-green;
41 | }
42 | }
43 | }
44 | .Description {
45 | font-family: Asap;
46 | font-size: 15px;
47 | line-height: 1.5;
48 | color: $color-dark-blue-100;
49 | }
50 | .textItem {
51 | font-family: Asap;
52 | font-size: 12.5px;
53 | line-height: 1.5;
54 | color: $color-dark-blue-100;
55 | margin-left: 8px;
56 | }
57 | .Force {
58 | display: flex;
59 | align-items: center;
60 | margin-bottom: 10px;
61 | }
62 | .ItemFlex {
63 | display: flex;
64 | height: 21px;
65 | align-items: center;
66 | }
67 | .LabelForce {
68 | font-family: Asap;
69 | font-size: 13px;
70 | color: $color-dark-blue-100;
71 | margin-left: 10px;
72 | }
73 | .Oval {
74 | width: 10px;
75 | height: 10px;
76 | border: solid 1px #c8cbd3;
77 | border-radius: 50%;
78 | background-color: #e0e1e6;
79 | }
80 | .Rectangle {
81 | width: 277.9px;
82 | height: 8px;
83 | border-radius: 4px;
84 | background-color: #e0e1e6;
85 | display: flex;
86 | align-items: center;
87 | }
88 | .RectangleForce {
89 | height: 8px;
90 | border-radius: 4px;
91 | width: 10px;
92 | background-color: $color-red;
93 | }
94 | .Force-1 {
95 | width: 10px;
96 | }
97 | .Force-2 {
98 | width: 103px;
99 | background-color: $color-yellow;
100 | }
101 | .Force-3 {
102 | width: 153px;
103 | background-color: $color-yellow;
104 | }
105 | .Force-4 {
106 | width: 203px;
107 | background-color: $color-yellow;
108 | }
109 | .Force-5 {
110 | width: 260px;
111 | background-color: $color-green;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/modals/SyncDevice/SyncDevice.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { RefreshCw, CheckCircle, XCircle } from 'react-feather'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | import { Modal, Button, Space, Typography } from '../../components'
7 |
8 | import styles from './SyncDevice.module.scss'
9 |
10 | const SyncDeviceModal = ({ children }) => {
11 | return
12 |
13 | {children}
14 |
15 |
16 | }
17 |
18 | const SyncDeviceModalSyncing = ({ t, setSyncStep }) => (
19 |
20 | {t('Synchronization in progress...')}
21 |
22 |
23 |
24 | {t('Please wait, we are retrieving your profile')}
25 |
26 | setSyncStep(1)}>{t('cancel')}
27 |
28 |
29 | )
30 |
31 | const SyncDeviceModalFinished = ({ t, setSyncStep }) => (
32 |
33 | {t('Synchronization finished!')}
34 |
35 |
36 |
37 | {t('You can now use your profile on your new device!')}
38 |
39 | setSyncStep(2)}>{t('close')}
40 |
41 |
42 | )
43 |
44 | const SyncDeviceModalError = ({ t, setSyncStep }) => (
45 |
46 | {t('Synchronization failure')}
47 |
48 |
49 |
50 | {t('We were unable to retrieve your profile, please try again.')}
51 |
52 | setSyncStep(0)}>{t('go back')}
53 |
54 |
55 | )
56 |
57 | const SyncDevice = () => {
58 | const [syncStep, setSyncStep] = useState(0)
59 | const { t } = useTranslation()
60 |
61 | switch (syncStep) {
62 | case 0:
63 | return
64 | case 1:
65 | return
66 | default:
67 | return
68 | }
69 | }
70 |
71 | SyncDeviceModalSyncing.propTypes =
72 | SyncDeviceModalFinished.propTypes =
73 | SyncDeviceModalError.propTypes = {
74 | t: PropTypes.func,
75 | setSyncStep: PropTypes.func
76 | }
77 |
78 | SyncDeviceModal.propTypes = {
79 | children: PropTypes.oneOfType([
80 | PropTypes.arrayOf(PropTypes.node),
81 | PropTypes.node
82 | ]).isRequired
83 | }
84 |
85 | export default SyncDevice
86 |
--------------------------------------------------------------------------------
/src/components/Typography/Typography.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .typography {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | .username {
9 | font-size: 18px;
10 | font-weight: 600;
11 | line-height: 1.11;
12 | color: white;
13 | max-width: 100%;
14 | text-align: center;
15 | word-wrap: break-word;
16 | }
17 |
18 | .username-alt {
19 | font-size: 14px;
20 | font-weight: 600;
21 | line-height: 1.43;
22 | color: white;
23 | text-transform: capitalize;
24 | }
25 |
26 | .title {
27 | font-size: 30px;
28 | line-height: 0.83;
29 | color: white;
30 | }
31 |
32 | .title-landing {
33 | font-family: 'Asap Condensed';
34 | font-size: 28px;
35 | font-weight: 600;
36 | line-height: 1.14;
37 | color: #00cafc;
38 | @media (max-width: 700px) {
39 | font-size: 22px;
40 | line-height: 1.36;
41 | }
42 | }
43 |
44 | .title-landing2 {
45 | font-family: 'Asap Condensed';
46 | font-size: 34px;
47 | font-weight: 600;
48 | font-stretch: condensed;
49 | line-height: 1.18;
50 | text-align: center;
51 | color: #252a39;
52 | @media (max-width: 700px) {
53 | font-size: 28px;
54 | line-height: 1.14;
55 | }
56 | }
57 |
58 | .title-page {
59 | font-size: 20px;
60 | font-weight: bold;
61 | letter-spacing: 0.1px;
62 | color: $color-dark-blue-100;
63 | }
64 |
65 | .subtitle-page {
66 | font-size: 14px;
67 | line-height: 1.36;
68 | color: $color-blue-grey;
69 | }
70 |
71 | .title-card {
72 | font-size: 20px;
73 | font-weight: 600;
74 | line-height: 1.25;
75 | color: $color-dark-blue-100;
76 | }
77 |
78 | .title-modal {
79 | font-size: 28px;
80 | font-weight: bold;
81 | color: $color-dark-blue-100;
82 | // max-width: 400px;
83 | text-align: center;
84 | }
85 |
86 | .paragraph {
87 | font-size: 14px;
88 | line-height: 1.36;
89 | color: $color-dark-blue-100;
90 | }
91 |
92 | .paragraph-landing {
93 | font-size: 18px;
94 | line-height: 1.22;
95 | color: #e0e1e6;
96 | }
97 |
98 | .paragraph-landing-dark {
99 | font-size: 18px;
100 | line-height: 1.22;
101 | color: #697496;
102 | }
103 |
104 | .paragraph-modal {
105 | font-size: 14px;
106 | line-height: 1.36;
107 | color: $color-dark-blue-100;
108 | }
109 |
110 | .label {
111 | text-transform: uppercase;
112 | font-size: 12px;
113 | font-weight: 500;
114 | }
115 |
116 | .label-nav {
117 | text-transform: uppercase;
118 | font-size: 10px;
119 | font-weight: 600;
120 | letter-spacing: 0.5px;
121 | color: white;
122 | }
123 |
124 | .footer {
125 | font-size: 14px;
126 | line-height: 1.36;
127 | color: $color-grey-300;
128 | }
129 |
130 | .textFieldButton {
131 | font-size: 12px;
132 | font-weight: 600;
133 | color: $color-dark-blue-100;
134 | }
135 |
136 | .avatarUsername {
137 | margin: 0;
138 | font-weight: bold;
139 | letter-spacing: 0.1px;
140 | font-size: 55px;
141 | color: white;
142 | }
143 |
144 | .line {
145 | width: 100%;
146 | display: flex;
147 | align-items: center;
148 | &:before,
149 | &:after {
150 | border-top: 1px solid $color-grey-200;
151 | content: "";
152 | flex: 1;
153 | margin: 0 12px;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/assets/head-landing-mobile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/src/assets/head-landing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import { Masq } from '../lib'
2 |
3 | const masq = new Masq()
4 |
5 | const receiveUsers = users => ({
6 | type: 'RECEIVE_USERS',
7 | users
8 | })
9 |
10 | const receiveApps = apps => ({
11 | type: 'RECEIVE_APPS',
12 | apps
13 | })
14 |
15 | export const setSyncStep = syncStep => {
16 | return {
17 | type: 'SET_SYNC_STEP',
18 | syncStep
19 | }
20 | }
21 |
22 | export const signin = (user, passphrase) => {
23 | return function (dispatch) {
24 | return masq.openProfile(user.id, passphrase)
25 | .then(profile => dispatch({
26 | type: 'SIGNIN',
27 | profile
28 | }))
29 | }
30 | }
31 |
32 | export const signout = () => {
33 | return function (dispatch) {
34 | return masq.closeProfile()
35 | .then(() => dispatch({
36 | type: 'SIGNOUT'
37 | }))
38 | }
39 | }
40 |
41 | export const updateUser = (id, update) => {
42 | const profile = { ...update, id }
43 | return function (dispatch) {
44 | return masq.updateProfile(profile)
45 | .then(() => dispatch({
46 | type: 'SIGNIN',
47 | profile
48 | }))
49 | }
50 | }
51 |
52 | export const signup = user => {
53 | const { password } = user
54 | return function (dispatch) {
55 | return masq.addProfile(user)
56 | .then((profile) => dispatch(signin(profile, password)))
57 | }
58 | }
59 |
60 | export const removeProfile = () => {
61 | return function (dispatch) {
62 | return masq.removeProfile()
63 | .then(() => dispatch(signout()))
64 | }
65 | }
66 |
67 | export const fetchUsers = () => {
68 | return function (dispatch) {
69 | return masq.getProfiles()
70 | .then(users => dispatch(receiveUsers(users)))
71 | }
72 | }
73 |
74 | export const addDevice = device => ({
75 | type: 'ADD_DEVICE',
76 | device
77 | })
78 |
79 | export const fetchApps = () => {
80 | return function (dispatch) {
81 | return masq.getApps()
82 | .then(apps => dispatch(receiveApps(apps)))
83 | }
84 | }
85 |
86 | export const removeApp = (app) => {
87 | return function (dispatch) {
88 | return masq.removeApp(app)
89 | .then(() => dispatch(fetchApps()))
90 | }
91 | }
92 |
93 | export const fetchCurrentAppRequestStatus = () => ({
94 | type: 'FETCH_CURRENT_APP_REQUEST_STATUS'
95 | })
96 |
97 | export const setCurrentAppRequest = app => {
98 | return {
99 | type: 'SET_CURRENT_APP_REQUEST',
100 | app
101 | }
102 | }
103 |
104 | export const updateCurrentAppRequest = update => {
105 | return {
106 | type: 'UPDATE_CURRENT_APP_REQUEST',
107 | update
108 | }
109 | }
110 |
111 | export const handleUserAppLogin = (channel, key, appId) => {
112 | return function (dispatch) {
113 | return masq.handleUserAppLogin(channel, key, appId)
114 | .then((info) => dispatch(updateCurrentAppRequest({
115 | ...info
116 | })))
117 | }
118 | }
119 |
120 | export const handleUserAppRegister = (isAccepted) => {
121 | return function (dispatch) {
122 | return masq.handleUserAppRegister(isAccepted)
123 | .then(() => dispatch(updateCurrentAppRequest({
124 | isConnected: isAccepted
125 | })))
126 | }
127 | }
128 |
129 | export const setNotification = (notification) => ({
130 | type: 'SET_NOTIFICATION',
131 | notification
132 | })
133 |
134 | export const setLoading = (loading) => ({
135 | type: 'SET_LOADING',
136 | loading
137 | })
138 |
139 | export const updatePassphrase = (oldPass, newPass) => {
140 | return function (dispatch) {
141 | return masq.updatePassphrase(oldPass, newPass)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/src/assets/logo-sidebar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/containers/Applications/Applications.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { Redirect } from 'react-router-dom'
5 | import { Trash } from 'react-feather'
6 | import { withTranslation } from 'react-i18next'
7 | import { fetchApps, removeApp, setNotification } from '../../actions'
8 | import { Card, Typography, Space } from '../../components'
9 | import { DeleteAppDialog } from '../../modals'
10 |
11 | import Link from './Link'
12 |
13 | import styles from './Applications.module.scss'
14 |
15 | class Apps extends PureComponent {
16 | constructor (props) {
17 | super(props)
18 | this.state = {
19 | appToRemove: null,
20 | confirmDialog: false
21 | }
22 |
23 | this.confirmDelete = this.confirmDelete.bind(this)
24 | this.handleTrashClick = this.handleTrashClick.bind(this)
25 | this.closeConfirmDialog = this.closeConfirmDialog.bind(this)
26 | }
27 |
28 | componentDidMount () {
29 | if (!this.props.user) return
30 | this.props.fetchApps()
31 | }
32 |
33 | handleTrashClick (app) {
34 | this.setState({
35 | appToRemove: app,
36 | confirmDialog: true
37 | })
38 | }
39 |
40 | async closeConfirmDialog () {
41 | this.setState({
42 | appToRemove: null,
43 | confirmDialog: false
44 | })
45 | }
46 |
47 | async confirmDelete () {
48 | const { removeApp, setNotification } = this.props
49 | const { appToRemove } = this.state
50 | await removeApp(appToRemove)
51 | this.closeConfirmDialog()
52 | setNotification({
53 | title: this.props.t('Application succesfully deleted')
54 | })
55 | }
56 |
57 | render () {
58 | const { apps, user, t } = this.props
59 | const { confirmDialog, appToRemove } = this.state
60 |
61 | if (!user) return
62 |
63 | return (
64 |
65 | {confirmDialog &&
this.confirmDelete()}
68 | onCancel={() => this.closeConfirmDialog()}
69 | onClose={() => this.closeConfirmDialog()}
70 | />}
71 |
72 |
73 | {t('My applications')}
74 |
75 |
76 |
77 |
78 | {apps.length === 0 && (
79 |
80 | {t('You do not have a registered application yet')}
81 |
82 | )}
83 |
84 |
85 | {apps.map((app, index) => (
86 |
87 | this.handleTrashClick(app)}
97 | />
98 | }
99 | footer={ }
100 | />
101 |
102 | ))}
103 |
104 |
105 | )
106 | }
107 | }
108 |
109 | const mapStateToProps = state => ({
110 | user: state.masq.currentUser,
111 | apps: state.masq.apps
112 | })
113 |
114 | const mapDispatchToProps = dispatch => ({
115 | fetchApps: () => dispatch(fetchApps()),
116 | removeApp: (app) => dispatch(removeApp(app)),
117 | setNotification: (notif) => dispatch(setNotification(notif))
118 | })
119 |
120 | Apps.propTypes = {
121 | apps: PropTypes.array,
122 | user: PropTypes.object,
123 | fetchApps: PropTypes.func,
124 | removeApp: PropTypes.func,
125 | setNotification: PropTypes.func,
126 | t: PropTypes.func
127 | }
128 | const translatedApps = withTranslation()(Apps)
129 | export default connect(mapStateToProps, mapDispatchToProps)(translatedApps)
130 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/PasswordStrength/PasswordStrength.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import cx from 'classnames'
4 | import { useTranslation } from 'react-i18next'
5 | import styles from './PasswordStrength.module.scss'
6 | import { Shield, Lock, Unlock, CheckCircle } from 'react-feather'
7 | const { getPasswordInfo, getForce } = require('../../lib/validators')
8 | const NonChecked = () => (
9 |
10 | )
11 |
12 | const Description = () => {
13 | const { t } = useTranslation()
14 | return (
15 |
16 | {t('To be completely secure, the key must contain at least')}:
17 |
18 | )
19 | }
20 |
21 | const Item = ({ fulfilled, text }) => {
22 | const { t } = useTranslation()
23 | let printedRule = null
24 | switch (text) {
25 | case 'lowercase':
26 | printedRule = t('1 lowercase')
27 | break
28 | case 'uppercase':
29 | printedRule = t('1 uppercase')
30 | break
31 | case 'number':
32 | printedRule = t('1 number')
33 | break
34 | case 'specialCharacter':
35 | printedRule = t('1 special character (!?$#@...)')
36 | break
37 | case 'secureLength':
38 | printedRule = t('12 characters')
39 | break
40 | default:
41 | break
42 | }
43 | const Icon = fulfilled
44 | ?
45 | :
46 | return (
47 |
48 | {Icon}
49 |
50 | {printedRule}
51 |
52 |
53 | )
54 | }
55 |
56 | Item.propTypes = {
57 | fulfilled: PropTypes.bool.isRequired,
58 | text: PropTypes.oneOf([
59 | 'lowercase',
60 | 'uppercase',
61 | 'number',
62 | 'specialCharacter',
63 | 'secureLength'
64 | ])
65 | }
66 |
67 | const ForceBar = ({ force }) => {
68 | return (
69 |
70 |
73 |
74 | {'Force'}
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | ForceBar.propTypes = {
82 | force: PropTypes.oneOf([
83 | 0,
84 | 1,
85 | 2,
86 | 3,
87 | 4,
88 | 5
89 | ])
90 | }
91 | ForceBar.defaultProps = {
92 | force: 0
93 | }
94 | const PasswordRules = ({ force, passwordInfo }) => {
95 | const lockIcon = force > 1
96 | ?
100 | :
104 | return (
105 |
106 |
107 | {Object.keys(passwordInfo).filter(elt => elt !== 'force').map(elt => {
108 | return (
109 | )
114 | })}
115 |
116 |
117 |
121 | {lockIcon}
122 |
123 |
124 |
125 | )
126 | }
127 |
128 | PasswordRules.propTypes = {
129 | passwordInfo: PropTypes.object,
130 | force: PropTypes.oneOf([
131 | 0,
132 | 1,
133 | 2,
134 | 3,
135 | 4,
136 | 5
137 | ])
138 | }
139 |
140 | const PasswordStrength = ({ password }) => {
141 | const passwordInfo = getPasswordInfo(password)
142 | const force = getForce(password)
143 |
144 | return (
145 |
150 | )
151 | }
152 |
153 | PasswordStrength.defaultProps = {
154 | password: ''
155 | }
156 |
157 | PasswordStrength.propTypes = {
158 | password: PropTypes.string.isRequired
159 | }
160 |
161 | export default PasswordStrength
162 |
--------------------------------------------------------------------------------
/src/assets/qwant.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | )
20 |
21 | export function register (config) {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
29 | return
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Let's check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl, config)
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | )
46 | })
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl, config)
50 | }
51 | })
52 | }
53 | }
54 |
55 | function registerValidSW (swUrl, config) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.')
69 |
70 | // Execute callback
71 | if (config.onUpdate) {
72 | config.onUpdate(registration)
73 | }
74 | } else {
75 | // At this point, everything has been precached.
76 | // It's the perfect time to display a
77 | // "Content is cached for offline use." message.
78 | console.log('Content is cached for offline use.')
79 |
80 | // Execute callback
81 | if (config.onSuccess) {
82 | config.onSuccess(registration)
83 | }
84 | }
85 | }
86 | }
87 | }
88 | })
89 | .catch(error => {
90 | console.error('Error during service worker registration:', error)
91 | })
92 | }
93 |
94 | function checkValidServiceWorker (swUrl, config) {
95 | // Check if the service worker can be found. If it can't reload the page.
96 | window.fetch(swUrl)
97 | .then(response => {
98 | // Ensure service worker exists, and that we really are getting a JS file.
99 | if (
100 | response.status === 404 ||
101 | response.headers.get('content-type').indexOf('javascript') === -1
102 | ) {
103 | // No service worker found. Probably a different app. Reload the page.
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister().then(() => {
106 | window.location.reload()
107 | })
108 | })
109 | } else {
110 | // Service worker found. Proceed as normal.
111 | registerValidSW(swUrl, config)
112 | }
113 | })
114 | .catch(() => {
115 | console.log(
116 | 'No internet connection found. App is running in offline mode.'
117 | )
118 | })
119 | }
120 |
121 | export function unregister () {
122 | if ('serviceWorker' in navigator) {
123 | navigator.serviceWorker.ready.then(registration => {
124 | registration.unregister()
125 | })
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/test/validators.test.js:
--------------------------------------------------------------------------------
1 | const { expect } = require('chai')
2 | const { isName, isUsername, getPasswordInfo, getForce, isForceEnough } = require('../src/lib/validators')
3 |
4 | describe('name validator', () => {
5 | it('should be valid', () => {
6 | const str = 'someName'
7 | expect(isName(str)).to.be.true
8 | })
9 |
10 | it('empty is allowed', () => {
11 | expect(isName('')).to.be.true
12 | })
13 |
14 | it('spaces are allowed', () => {
15 | const str = 'some name'
16 | expect(isName(str)).to.be.true
17 | })
18 |
19 | it('accents are allowed', () => {
20 | const str = 'some name é'
21 | expect(isName(str)).to.be.true
22 | })
23 |
24 | it('numbers are forbidden', () => {
25 | const str = 'some name 2'
26 | expect(isName(str)).to.be.false
27 | })
28 |
29 | it('special characters are forbidden', () => {
30 | const str = 'some name$'
31 | expect(isName(str)).to.be.false
32 | })
33 | })
34 |
35 | describe('username validator', () => {
36 | it('should be valid', () => {
37 | const str = 'someUsername'
38 | expect(isUsername(str)).to.be.true
39 | })
40 |
41 | it('empty is forbidden', () => {
42 | expect(isUsername('')).to.be.false
43 | })
44 |
45 | it('accents are forbidden', () => {
46 | expect(isUsername('é')).to.be.false
47 | })
48 |
49 | it('spaces are not allowed', () => {
50 | const str = 'some username'
51 | expect(isUsername(str)).to.be.false
52 | })
53 |
54 | it('numbers are allowed', () => {
55 | const str = 'someUsername75'
56 | expect(isUsername(str)).to.be.true
57 | })
58 |
59 | it('special characters are allowed', () => {
60 | const str = 'someUsername$'
61 | expect(isUsername(str)).to.be.true
62 | })
63 | })
64 |
65 | describe('password rules checker', () => {
66 | it('should return true if password contains lowercase', () => {
67 | const str = 'somePassword9$'
68 | expect(getPasswordInfo(str).lowercase).to.be.true
69 | })
70 | it('should return true if password contains uppercase', () => {
71 | const str = 'somePassword9$'
72 | expect(getPasswordInfo(str).uppercase).to.be.true
73 | })
74 | it('should return true if password contains specialCharacter', () => {
75 | const str = 'somePassword9$'
76 | expect(getPasswordInfo(str).specialCharacter).to.be.true
77 | })
78 | it('should return true if password contains specialCharacter', () => {
79 | const str = 'somePassword9$'
80 | expect(getPasswordInfo(str).specialCharacter).to.be.true
81 | })
82 | it('should return true if password contains specialCharacter =', () => {
83 | const str = 'somePassword9='
84 | expect(getPasswordInfo(str).specialCharacter).to.be.true
85 | })
86 | it('should return true if password contains specialCharacter `', () => {
87 | const str = 'somePassword9`'
88 | expect(getPasswordInfo(str).specialCharacter).to.be.true
89 | })
90 | it('should return true if password contains specialCharacter "', () => {
91 | const str = 'somePassword9"'
92 | expect(getPasswordInfo(str).specialCharacter).to.be.true
93 | })
94 | it('should return false if password contains a special Character not in the whitelist : ex ² "', () => {
95 | const str = 'somePassword²'
96 | expect(getPasswordInfo(str).specialCharacter).to.be.false
97 | })
98 | it('should return true if password contains number', () => {
99 | const str = 'somePassword9$'
100 | expect(getPasswordInfo(str).number).to.be.true
101 | })
102 | it('should return true if password contains at least 12 characters', () => {
103 | const str = 'somePassword9$'
104 | expect(getPasswordInfo(str).secureLength).to.be.true
105 | })
106 |
107 | it('should return 0 for the password force', () => {
108 | const str = 'some'
109 | expect(getForce(str)).to.equal(0)
110 | })
111 | it('should return 1 for the password force', () => {
112 | const str = 'somepa'
113 | expect(getForce(str)).to.equal(1)
114 | })
115 | it('should return 2 for the password force', () => {
116 | const str = 'somEpa'
117 | expect(getForce(str)).to.equal(2)
118 | })
119 | it('should return 1 for the password force', () => {
120 | const str = '145689'
121 | expect(getForce(str)).to.equal(1)
122 | })
123 | it('should return 2 for the password force', () => {
124 | const str = '14568A'
125 | expect(getForce(str)).to.equal(2)
126 | })
127 | it('should return 5 for the password force', () => {
128 | const str = 'somePassword9$'
129 | expect(getForce(str)).to.equal(5)
130 | })
131 | })
132 |
133 | describe('password force validator', () => {
134 | it('should accept low + upp', () => {
135 | const str = 'abcdeF'
136 | expect(isForceEnough(str)).to.be.true
137 | })
138 | it('should accept low + number', () => {
139 | const str = 'abcde5'
140 | expect(isForceEnough(str)).to.be.true
141 | })
142 | it('should accept low + special', () => {
143 | const str = 'abcde#'
144 | expect(isForceEnough(str)).to.be.true
145 | })
146 | it('should not accept only low', () => {
147 | const str = 'abcdef'
148 | expect(isForceEnough(str)).to.be.false
149 | })
150 | it('should not accept only upp', () => {
151 | const str = 'ABCDEF'
152 | expect(isForceEnough(str)).to.be.false
153 | })
154 | it('should not accept only numbers', () => {
155 | const str = '123456'
156 | expect(isForceEnough(str)).to.be.false
157 | })
158 | it('should not accept if length < 6', () => {
159 | const str = '1a#b'
160 | expect(isForceEnough(str)).to.be.false
161 | })
162 | })
163 |
--------------------------------------------------------------------------------
/src/modals/PasswordEdit/PasswordEdit.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { withTranslation } from 'react-i18next'
5 |
6 | import { updatePassphrase, setNotification } from '../../actions'
7 | import { Modal, Button, TextField, Typography, Space, PasswordStrength } from '../../components'
8 | import { isForceEnough } from '../../lib/validators'
9 |
10 | import styles from './PasswordEdit.module.scss'
11 |
12 | class PasswordEdit extends React.Component {
13 | constructor (props) {
14 | super(props)
15 | this.state = {
16 | currentPassword: '',
17 | password: '',
18 | passwordConfirmation: '',
19 | showErrors: false,
20 | currentPasswordError: false
21 | }
22 |
23 | this.onChange = this.onChange.bind(this)
24 | this.handleKeyUp = this.handleKeyUp.bind(this)
25 | this.updatePassphrase = this.updatePassphrase.bind(this)
26 | }
27 |
28 | handleKeyUp (e) {
29 | if (e.key !== 'Enter') {
30 | return
31 | }
32 | this.updatePassphrase()
33 | }
34 |
35 | isValid (fieldName) {
36 | const field = this.state[fieldName]
37 |
38 | if (!this.state.showErrors) {
39 | // Don't show error as long as the user does not click Finish btn
40 | return true
41 | }
42 |
43 | switch (fieldName) {
44 | case 'password': return isForceEnough(field) && this.state.password !== this.state.currentPassword
45 | case 'passwordConfirmation': return field === this.state.password
46 | default: return false
47 | }
48 | }
49 |
50 | onChange (field, e) {
51 | this.setState({
52 | [field]: e.target.value
53 | })
54 | }
55 |
56 | async updatePassphrase () {
57 | const { currentPassword, password } = this.state
58 | const { t } = this.props
59 | this.validationEnabled = true
60 |
61 | this.setState({
62 | currentPasswordError: false
63 | })
64 |
65 | if (!isForceEnough(password) ||
66 | (password === currentPassword)) {
67 | return this.setState({
68 | showErrors: true
69 | })
70 | }
71 |
72 | try {
73 | await this.props.updatePassphrase(currentPassword, password)
74 | this.props.onClose()
75 | this.props.setNotification({
76 | error: false,
77 | title: t('Secret key succesfully updated.')
78 | })
79 | } catch (err) {
80 | this.setState({
81 | currentPasswordError: true
82 | })
83 | }
84 | }
85 |
86 | render () {
87 | const { currentPassword, password, passwordConfirmation, currentPasswordError } = this.state
88 | const { onClose, t } = this.props
89 |
90 | return (
91 |
92 |
93 |
{t('Update your secret key')}
94 |
95 |
this.onChange('currentPassword', e)}
106 | />
107 |
108 | this.onChange('password', e)}
119 | />
120 |
121 |
122 |
123 | this.onChange('passwordConfirmation', e)}
134 | />
135 |
136 |
137 |
138 |
139 | {t('Cancel')}
140 | {t('Save')}
141 |
142 |
143 |
144 | )
145 | }
146 | }
147 |
148 | PasswordEdit.propTypes = {
149 | onClose: PropTypes.func,
150 | updatePassphrase: PropTypes.func.isRequired,
151 | setNotification: PropTypes.func.isRequired,
152 | t: PropTypes.func
153 | }
154 |
155 | const mapDispatchToProps = dispatch => ({
156 | updatePassphrase: (oldPass, newPass) => dispatch(updatePassphrase(oldPass, newPass)),
157 | setNotification: (notif) => dispatch(setNotification(notif))
158 | })
159 | const translatedPasswordEdit = withTranslation()(PasswordEdit)
160 | export default connect(null, mapDispatchToProps)(translatedPasswordEdit)
161 |
--------------------------------------------------------------------------------
/src/assets/hard-disk.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------
/src/containers/Landing/Landing.module.scss:
--------------------------------------------------------------------------------
1 | @import '../../styles/colors.scss';
2 |
3 | .Landing {
4 | width: 100%;
5 | background-color: $color-dark-blue-100;
6 |
7 | @media (max-width: 700px) {
8 | text-align: center;
9 | }
10 |
11 | svg {
12 | max-width: 100%;
13 | }
14 |
15 | .section1 {
16 | padding: 68px 20px 0 20px;
17 | min-height: 660px;
18 | // background-color: $color-dark-blue-100;
19 | background-repeat: no-repeat;
20 | background-size: cover;
21 | background-position: bottom center;
22 |
23 | @media (max-width: 700px) {
24 | .Logo {
25 | margin: auto;
26 | max-width: 36%;
27 | }
28 | }
29 |
30 | .connectBtn {
31 | position: absolute;
32 | z-index: 3;
33 | top: 27px;
34 | right: 48px;
35 | }
36 |
37 | .title {
38 | margin: auto;
39 | max-width: 689px;
40 | p {
41 | font-family: 'Asap Condensed';
42 | margin: 0;
43 | font-size: 40px;
44 | font-weight: 600;
45 | line-height: 1.13;
46 | text-align: center;
47 | color: white;
48 |
49 | @media (max-width: 700px) {
50 | font-size: 28px;
51 | line-height: 1.25;
52 | }
53 | }
54 | }
55 |
56 | .accountBtn {
57 | display: flex;
58 | justify-content: center;
59 | }
60 | }
61 |
62 | .text {
63 | max-width: 380px;
64 |
65 | :first-child {
66 | margin-bottom: 32px;
67 |
68 | @media (max-width: 700px) {
69 | margin-bottom: 12px;
70 | }
71 | }
72 | }
73 |
74 | .section2 {
75 | width: 100%;
76 | margin-top: -160px;
77 |
78 | .Box {
79 | display: flex;
80 | padding: 0 16px;
81 | justify-content: space-between;
82 | align-items: center;
83 | margin: auto;
84 | margin-bottom: 26px;
85 | max-width: 680px;
86 |
87 | :first-child {
88 | margin-right: 16px;
89 | }
90 |
91 | @media (max-width: 700px) {
92 | margin-bottom: 48px;
93 | flex-direction: column-reverse;
94 | .text {
95 | margin-top: 28px;
96 | }
97 |
98 | svg {
99 | max-width: 35%;
100 | height: 100%;
101 | }
102 | }
103 | }
104 |
105 | .Hdd {
106 | display: flex;
107 | padding: 0 16px;
108 | justify-content: space-between;
109 | align-items: center;
110 | margin: auto;
111 | max-width: 740px;
112 | margin-bottom: 95px;
113 |
114 | :first-child {
115 | margin-right: 16px;
116 | }
117 |
118 | @media (max-width: 700px) {
119 | margin-bottom: 64px;
120 | flex-direction: column;
121 | .text {
122 | margin-top: 28px;
123 | }
124 |
125 | svg {
126 | max-width: 50%;
127 | max-height: 100%;
128 | }
129 | }
130 | }
131 |
132 | .Devices {
133 | display: flex;
134 | padding: 0 16px;
135 | justify-content: space-between;
136 | align-items: center;
137 | margin: auto;
138 | max-width: 880px;
139 |
140 | :first-child {
141 | margin-right: 16px;
142 | }
143 |
144 | @media (max-width: 700px) {
145 | flex-direction: column-reverse;
146 | .text {
147 | margin-top: 28px;
148 | }
149 | }
150 | }
151 | }
152 |
153 | .section3 {
154 | margin-top: 76px;
155 | padding-top: 218px;
156 | padding-bottom: 112px;
157 | background-repeat: no-repeat;
158 | background-size: cover;
159 | background-position: top center;
160 |
161 | .title {
162 | margin: auto;
163 | max-width: 500px;
164 | padding: 16px;
165 | margin-bottom: 62px;
166 | @media (max-width: 700px) {
167 | margin-bottom: 0;
168 | }
169 | }
170 |
171 | .Shield {
172 | display: flex;
173 | padding: 0 16px;
174 | justify-content: space-between;
175 | align-items: center;
176 | margin: auto;
177 | max-width: 860px;
178 | margin-bottom: 100px;
179 |
180 | :first-child {
181 | margin-right: 16px;
182 | }
183 |
184 | @media (max-width: 700px) {
185 | flex-direction: column;
186 | margin-bottom:16px;
187 | svg {
188 | max-width: 214px;
189 | }
190 | }
191 | }
192 |
193 | .Windows {
194 | display: flex;
195 | padding: 0 16px;
196 | justify-content: space-between;
197 | align-items: center;
198 | margin: auto;
199 | max-width: 840px;
200 |
201 | :first-child {
202 | margin-right: 16px;
203 | }
204 |
205 | @media (max-width: 700px) {
206 | flex-direction: column-reverse;
207 |
208 | svg {
209 | max-width: 222px;
210 | }
211 | }
212 | }
213 | }
214 |
215 | .Footer {
216 | display: flex;
217 | align-items: center;
218 | height: 77px;
219 | margin-left: 34px;
220 | margin-right: 96px;
221 | justify-content: space-between;
222 |
223 | :first-child {
224 | width: unset;
225 | }
226 |
227 | a {
228 | text-decoration: none;
229 | }
230 |
231 | @media (max-width: 700px) {
232 | height: 100%;
233 | margin-right: 16px;
234 | align-items: baseline;
235 | padding-top: 32px;
236 | padding-bottom: 32px;
237 | }
238 |
239 | .links {
240 | display: flex;
241 | flex-direction: row;
242 | *:not(:first-child) {
243 | margin-left: 20px;
244 | }
245 |
246 | @media (max-width: 700px) {
247 | flex-direction: column;
248 | text-align: right;
249 | *:not(:last-child) {
250 | margin-bottom: 20px;
251 | }
252 | }
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/containers/Landing/Landing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { useTranslation } from 'react-i18next'
4 |
5 | import Head from '../../assets/head-landing.svg'
6 | import HeadMobile from '../../assets/head-landing-mobile.svg'
7 | import BGLanding from '../../assets/bg-landing.svg'
8 |
9 | import { ReactComponent as Logo } from '../../assets/logo.svg'
10 | import { ReactComponent as Box } from '../../assets/box.svg'
11 | import { ReactComponent as HDD } from '../../assets/hard-disk.svg'
12 | import { ReactComponent as Devices } from '../../assets/devices.svg'
13 | import { ReactComponent as Shield } from '../../assets/shield.svg'
14 | import { ReactComponent as Windows } from '../../assets/windows-masq.svg'
15 |
16 | import { Button, Space, Typography } from '../../components'
17 | import { useWindowWidth } from '../../hooks'
18 |
19 | import styles from './Landing.module.scss'
20 |
21 | // const remoteWebRTCEnabled = (process.env.REACT_APP_REMOTE_WEBRTC === 'true')
22 |
23 | const MOBILE_WIDTH = 700
24 |
25 | const Landing = ({ onClick, children }) => {
26 | const { t } = useTranslation()
27 | const width = useWindowWidth()
28 |
29 | return (
30 |
31 |
MOBILE_WIDTH ? Head : HeadMobile})` }}
34 | >
35 |
36 | {/* {remoteWebRTCEnabled &&
Connexion
} */}
37 |
38 |
39 | {children || (
40 |
41 |
42 |
{t('Free and secured storage of your preferences and personal data on all your devices')}
43 |
44 |
45 |
46 | {t('Create a new profile')}
47 |
48 |
49 | )}
50 |
51 |
52 |
53 |
54 |
55 | {t('Respecting your privacy')}
56 |
57 | {t('This application allows you to store all your preferences while guaranteeing your privacy')}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {t('Personal data is stored on your devices')}
67 |
68 | {t('No more need for the Cloud! Your preferences and personnal data are stored directly on your devices, they are encrypted to gurantee their security. You are the owner of your data')}
69 |
70 |
71 |
72 |
73 |
74 |
75 | {t('Real time synchronization between devices (coming soon)')}
76 |
77 | {t('Soon you can synchronize your profile between all your devices in real time without any Cloud!')}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | {t('Good practices for optimal use')}
89 |
90 |
91 |
92 |
93 |
94 |
95 | {t('Your secret key is only known by YOU')}
96 |
97 | {t('So, do not forget it :-) Your data is encrypted and decrypted on your device. That is why your secret key is never sent to our servers, we don\'t have access to it!')}
98 |
99 |
100 |
101 |
102 |
103 |
104 | {t('Keep this application window in the background in order to allow data synchronization')}
105 |
106 | {t('To benefit from real time synchronization and always have up-to-date data, keep this application window opened in the background on all your devices')}
107 |
108 |
109 |
110 |
111 |
112 |
113 |
132 |
133 | )
134 | }
135 |
136 | Landing.propTypes = {
137 | onClick: PropTypes.func,
138 | children: PropTypes.element
139 | }
140 |
141 | export default Landing
142 |
--------------------------------------------------------------------------------
/src/assets/apps-maps.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | import { createHashHistory } from 'history'
5 | import { Router, Route, Redirect } from 'react-router-dom'
6 | import DetectBrowser from 'detect-browser'
7 | import { withTranslation } from 'react-i18next'
8 | import * as common from 'masq-common'
9 |
10 | import { Login, Applications, Devices, Profile, Navbar, Loading } from './containers'
11 | import { Notification } from './components'
12 | import { addDevice, setCurrentAppRequest, setLoading, setNotification } from './actions'
13 | import { AuthApp, PersistentStorageRequest, UnsupportedBrowser } from './modals'
14 | import { isBrowserSupported } from './lib/browser'
15 | import { capitalize } from './lib/utils'
16 |
17 | import styles from './App.module.scss'
18 |
19 | const history = createHashHistory()
20 | const { MasqError } = common.errors
21 |
22 | // listen for errors event and display them
23 |
24 | const authenticatedRoutes = [
25 | {
26 | path: '/apps',
27 | sidebar: Navbar,
28 | main: Applications
29 | },
30 | {
31 | path: '/devices',
32 | sidebar: Navbar,
33 | main: Devices
34 | },
35 | {
36 | path: '/profile',
37 | sidebar: Navbar,
38 | main: Profile
39 | }
40 | ]
41 |
42 | class App extends Component {
43 | constructor () {
44 | super()
45 |
46 | this.dbMasqPrivate = null
47 | this.dbMasqPublic = null
48 | this.dbs = {} // all replicated dbs
49 | this.sw = null
50 |
51 | this.state = {
52 | persistentStorageRequest: false,
53 | messages: [],
54 | hash: null,
55 | prevPath: '',
56 | unsupportedBrowserModal: !isBrowserSupported()
57 | }
58 |
59 | this.handlePersistentStorageRequestClose = this.handlePersistentStorageRequestClose.bind(this)
60 | }
61 |
62 | async checkPersistentStorage () {
63 | const { name } = DetectBrowser.detect()
64 | if (name !== 'firefox') return
65 |
66 | if (!navigator.storage || !navigator.storage.persist) return
67 | const persistent = await navigator.storage.persisted()
68 | if (persistent) { return }
69 |
70 | this.setState({ persistentStorageRequest: true })
71 | const p = await navigator.storage.persist()
72 | if (p) {
73 | this.setState({ persistentStorageRequest: false })
74 | }
75 | }
76 |
77 | async componentDidMount () {
78 | console.log(`Masq version: ${process.env.REACT_APP_GIT_SHA}`)
79 | const { t } = this.props
80 | window.addEventListener('MasqError', (e) => {
81 | if (e.detail === MasqError.REPLICATION_SIGNALLING_ERROR) {
82 | this.props.setNotification({
83 | error: true,
84 | title: t('Connection failure, please retry.')
85 | })
86 | }
87 | })
88 |
89 | if (!this.props.devices.length) {
90 | const { name, os } = DetectBrowser.detect()
91 | this.props.addDevice({
92 | name: `${capitalize(name)} ${t('on')} ${os}`,
93 | description: t('This device'),
94 | color: '#40ae6c'
95 | })
96 | }
97 |
98 | history.listen(location => {
99 | const paths = ['/apps', '/devices', '/profile']
100 | if (location.pathname !== this.state.prevPath &&
101 | paths.includes(location.pathname)) {
102 | this.setState({
103 | prevPath: location.pathname
104 | })
105 | }
106 | })
107 |
108 | // this.checkPersistentStorage()
109 |
110 | this.props.setLoading(false)
111 | }
112 |
113 | handlePersistentStorageRequestClose () {
114 | this.setState({ persistentStorageRequest: false })
115 | }
116 |
117 | componentDidUpdate (prevProps) {
118 | const { prevPath } = this.state
119 | if (prevProps.loading && !this.props.loading) {
120 | history.push(prevPath)
121 | }
122 | }
123 |
124 | render () {
125 | const { persistentStorageRequest, unsupportedBrowserModal } = this.state
126 | const { currentUser, currentAppRequest, notification, setCurrentAppRequest, loading } = this.props
127 | const { pathname } = history.location
128 |
129 | if (unsupportedBrowserModal) {
130 | return
131 | }
132 |
133 | return (
134 |
135 |
136 | {loading && pathname !== '/loading' &&
}
137 | {notification &&
}
138 | {currentUser && currentAppRequest &&
139 |
setCurrentAppRequest(null)}
141 | appRequest={currentAppRequest}
142 | />
143 | }
144 |
145 | {persistentStorageRequest && }
146 |
147 |
148 |
149 |
150 |
151 |
152 | {authenticatedRoutes.map((route, index) => (
153 |
158 | ))}
159 |
160 | {authenticatedRoutes.map((route, index) => (
161 |
166 | ))}
167 |
168 |
169 |
170 | )
171 | }
172 | }
173 |
174 | const mapStateToProps = state => ({
175 | currentAppRequest: state.masq.currentAppRequest,
176 | currentUser: state.masq.currentUser,
177 | devices: state.masq.devices,
178 | users: state.masq.users,
179 | notification: state.notification.currentNotification,
180 | loading: state.loading.loading
181 | })
182 |
183 | const mapDispatchToProps = dispatch => ({
184 | addDevice: device => dispatch(addDevice(device)),
185 | setCurrentAppRequest: app => dispatch(setCurrentAppRequest(app)),
186 | setLoading: value => dispatch(setLoading(value)),
187 | setNotification: notif => dispatch(setNotification(notif))
188 | })
189 |
190 | App.propTypes = {
191 | currentUser: PropTypes.object,
192 | currentAppRequest: PropTypes.object,
193 | setCurrentAppRequest: PropTypes.func,
194 | addDevice: PropTypes.func,
195 | devices: PropTypes.arrayOf(PropTypes.object),
196 | notification: PropTypes.object,
197 | loading: PropTypes.bool,
198 | setLoading: PropTypes.func,
199 | setNotification: PropTypes.func,
200 | t: PropTypes.func
201 | }
202 | const translatedApp = withTranslation()(App)
203 | export default connect(mapStateToProps, mapDispatchToProps)(translatedApp)
204 |
--------------------------------------------------------------------------------