diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 7595f61c..ef15d7fc 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -16,6 +16,7 @@ const transactions = require('../transactions') const funding = require('../funding') const supervisor = require('../supervisor') const serverLogs = require('../server-logs') +const pairing = require('../pairing') const { accounts, coins, countries, currencies, languages } = require('../config') // TODO why does server logs messages can be null? @@ -186,6 +187,7 @@ const typeDefs = gql` machineSupportLogs(deviceId: ID!): SupportLogsResponse serverSupportLogs: SupportLogsResponse saveConfig(config: JSONObject): JSONObject + createPairingTotem(name: String!): String } ` @@ -215,6 +217,7 @@ const resolvers = { Mutation: { machineAction: (...[, { deviceId, action }]) => machineAction({ deviceId, action }), machineSupportLogs: (...[, { deviceId }]) => supportLogs.insert(deviceId), + createPairingTotem: (...[, { name }]) => pairing.totem(name), serverSupportLogs: () => serverLogs.insert(), saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config) .then(it => { diff --git a/lib/new-admin/pairing.js b/lib/new-admin/pairing.js new file mode 100644 index 00000000..a91800b9 --- /dev/null +++ b/lib/new-admin/pairing.js @@ -0,0 +1,33 @@ +const fs = require('fs') +const pify = require('pify') +const readFile = pify(fs.readFile) +const crypto = require('crypto') +const baseX = require('base-x') + +const options = require('../options') +const db = require('../db') +const pairing = require('../pairing') + +const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:' +const bsAlpha = baseX(ALPHA_BASE) + +const unpair = pairing.unpair + +function totem (name) { + const caPath = options.caPath + + return readFile(caPath) + .then(data => { + const caHash = crypto.createHash('sha256').update(data).digest() + const token = crypto.randomBytes(32) + const hexToken = token.toString('hex') + const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex') + const buf = Buffer.concat([caHash, token, Buffer.from(options.hostname)]) + const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)' + + return db.none(sql, [hexToken, caHexToken, name]) + .then(() => bsAlpha.encode(buf)) + }) +} + +module.exports = { totem, unpair } diff --git a/new-lamassu-admin/src/components/Header.js b/new-lamassu-admin/src/components/Header.js index 3df491e5..5f3595ee 100644 --- a/new-lamassu-admin/src/components/Header.js +++ b/new-lamassu-admin/src/components/Header.js @@ -4,6 +4,7 @@ import React, { memo, useState } from 'react' import { NavLink } from 'react-router-dom' import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg' +import AddMachine from 'src/pages/AddMachine' import styles from './Header.styles' import { Link } from './buttons' @@ -11,8 +12,8 @@ import { H4 } from './typography' const useStyles = makeStyles(styles) -const renderSubheader = (item, classes) => { - if (!item || !item.children) return false +const Subheader = ({ item, classes }) => { + const [open, setOpen] = useState(false) return (
@@ -31,7 +32,10 @@ const renderSubheader = (item, classes) => {
- Add Machine + setOpen(true)}> + Add Machine + + {open && setOpen(false)} />}
@@ -73,7 +77,9 @@ const Header = memo(({ tree }) => { - {renderSubheader(active, classes)} + {active && active.children && ( + + )} ) }) diff --git a/new-lamassu-admin/src/components/inputs/formik/index.js b/new-lamassu-admin/src/components/inputs/formik/index.js index e2612932..66e1e1e2 100644 --- a/new-lamassu-admin/src/components/inputs/formik/index.js +++ b/new-lamassu-admin/src/components/inputs/formik/index.js @@ -1,3 +1,4 @@ import Checkbox from './Checkbox' +import TextInput from './TextInput' -export { Checkbox } +export { Checkbox, TextInput } diff --git a/new-lamassu-admin/src/pages/AddMachine/AddMachine.js b/new-lamassu-admin/src/pages/AddMachine/AddMachine.js new file mode 100644 index 00000000..57b496d3 --- /dev/null +++ b/new-lamassu-admin/src/pages/AddMachine/AddMachine.js @@ -0,0 +1,200 @@ +import React, { memo, useState } from 'react' +import QRCode from 'qrcode.react' +import classnames from 'classnames' +import { Form, Formik, FastField } from 'formik' +import { makeStyles } from '@material-ui/core/styles' +import * as Yup from 'yup' +import { gql } from 'apollo-boost' +import { useMutation } from '@apollo/react-hooks' +import { Dialog, DialogContent, SvgIcon, IconButton } from '@material-ui/core' + +import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg' +import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg' +import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg' +import { primaryColor } from 'src/styling/variables' +import Title from 'src/components/Title' +import Sidebar from 'src/components/Sidebar' +import { Info2, P } from 'src/components/typography' +import { TextInput } from 'src/components/inputs/formik' +import { Button } from 'src/components/buttons' +import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg' +import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' + +import styles from './styles' + +const SAVE_CONFIG = gql` + mutation createPairingTotem($name: String!) { + createPairingTotem(name: $name) + } +` + +const useStyles = makeStyles(styles) + +const QrCodeComponent = ({ classes, qrCode, close }) => { + const [doneButton, setDoneButton] = useState(null) + setTimeout(() => setDoneButton(true), 2000) + + return ( + <> + + Scan QR code with your new cryptomat + +
+
+ +
+
+
+ +
+

+ To pair the machine you need scan the QR code with your machine. To + do this either snap a picture of this QR code or download it through + the button above and scan it with the scanning bay on your machine. +

+
+
+ {doneButton && ( +
+ +
+ )} + + ) +} + +const initialValues = { + name: '' +} + +const validationSchema = Yup.object().shape({ + name: Yup.string() + .required() + .max(50, 'Too long') +}) + +const MachineNameComponent = ({ nextStep, classes, setQrCode }) => { + const [register] = useMutation(SAVE_CONFIG, { + onCompleted: data => { + if (process.env.NODE_ENV === 'development') { + console.log('totem: ', data.createPairingTotem) + } + setQrCode(data.createPairingTotem) + nextStep() + }, + onError: e => console.log(e) + }) + + return ( + <> + + Machine Name (ex: Coffee shop 01) + + { + register({ variables: { name } }) + }}> +
+
+ +
+
+ +
+
+
+ + ) +} + +const steps = [ + { + label: 'Machine name', + component: MachineNameComponent + }, + { + label: 'Scan QR code', + component: QrCodeComponent + } +] + +const renderStepper = (step, it, idx, classes) => { + const active = step === idx + const past = idx < step + const future = idx > step + + return ( +
+ + {it.label} + + {active && } + {past && } + {future && } + {idx < steps.length - 1 && ( +
+ )} +
+ ) +} + +const AddMachine = memo(({ close }) => { + const classes = useStyles() + const [qrCode, setQrCode] = useState(null) + const [step, setStep] = useState(0) + + const Component = steps[step].component + return ( + + +
+
+ Add Machine + + + + + +
+
+ + {steps.map((it, idx) => renderStepper(step, it, idx, classes))} + +
+ setStep(step + 1)} + /> +
+
+
+
+
+ ) +}) + +export default AddMachine diff --git a/new-lamassu-admin/src/pages/AddMachine/index.js b/new-lamassu-admin/src/pages/AddMachine/index.js new file mode 100644 index 00000000..aae56dd4 --- /dev/null +++ b/new-lamassu-admin/src/pages/AddMachine/index.js @@ -0,0 +1,3 @@ +import AddMachine from './AddMachine' + +export default AddMachine diff --git a/new-lamassu-admin/src/pages/AddMachine/styles.js b/new-lamassu-admin/src/pages/AddMachine/styles.js new file mode 100644 index 00000000..9bd9ce4a --- /dev/null +++ b/new-lamassu-admin/src/pages/AddMachine/styles.js @@ -0,0 +1,99 @@ +import { + placeholderColor, + backgroundColor, + primaryColor, + mainWidth +} from 'src/styling/variables' +import typographyStyles from 'src/components/typography/styles' + +const { tl2, p } = typographyStyles + +const fill = '100%' +const flexDirection = 'column' + +const styles = { + dialog: { + backgroundColor, + width: fill, + minHeight: fill, + display: 'flex', + flexDirection, + padding: 0 + }, + wrapper: { + width: mainWidth, + height: fill, + margin: '0 auto', + flex: 1, + display: 'flex', + flexDirection + }, + contentDiv: { + display: 'flex', + flex: 1, + flexDirection: 'row' + }, + headerDiv: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center' + }, + contentWrapper: { + marginLeft: 48 + }, + button: { + marginTop: 64 + }, + nameTitle: { + marginTop: 16, + marginBottom: 25 + }, + qrTitle: { + marginTop: 12, + marginBottom: 40 + }, + qrCodeWrapper: { + display: 'flex' + }, + qrTextWrapper: { + width: 381, + marginLeft: 80, + display: 'flex' + }, + qrTextIcon: { + marginRight: 16 + }, + qrText: { + marginTop: 0 + }, + item: { + position: 'relative', + margin: '12px 0 12px 0', + display: 'flex' + }, + itemText: { + extend: p, + color: placeholderColor, + marginRight: 24 + }, + itemTextActive: { + extend: tl2, + color: primaryColor + }, + itemTextPast: { + color: primaryColor + }, + stepperPath: { + position: 'absolute', + height: 25, + width: 1, + border: [[1, 'solid', placeholderColor]], + right: 7, + top: 15 + }, + stepperPast: { + border: [[1, 'solid', primaryColor]] + } +} + +export default styles