moved l-a-s in here

This commit is contained in:
Josh Harvey 2016-12-05 17:15:32 +02:00
parent 1e3e55e362
commit 836ab07776
18 changed files with 3946 additions and 281 deletions

184
bin/lamassu-admin-server Executable file
View file

@ -0,0 +1,184 @@
#!/usr/bin/env node
const os = require('os')
const fs = require('fs')
const path = require('path')
const express = require('express')
const app = express()
const https = require('https')
const http = require('http')
const bodyParser = require('body-parser')
const serveStatic = require('serve-static')
const cookieParser = require('cookie-parser')
const argv = require('minimist')(process.argv.slice(2))
const got = require('got')
const morgan = require('morgan')
const accounts = require('../lib/admin/accounts')
const machines = require('../lib/admin/machines')
const config = require('../lib/admin/config')
const login = require('../lib/admin/login')
const pairing = require('../lib/admin/pairing')
const server = require('../lib/admin/server')
const transactions = require('../lib/admin/transactions')
const devMode = argv.dev
let serverConfig
try {
const homeConfigPath = path.resolve(os.homedir(), '.lamassu', 'lamassu.json')
serverConfig = JSON.parse(fs.readFileSync(homeConfigPath))
} catch (_) {
try {
const globalConfigPath = path.resolve('/etc', 'lamassu', 'lamassu.json')
serverConfig = JSON.parse(fs.readFileSync(globalConfigPath))
} catch (_) {
console.error("Couldn't open config file.")
process.exit(1)
}
}
const hostname = serverConfig.hostname
if (!hostname) {
console.error('Error: no hostname specified.')
process.exit(1)
}
function dbNotify () {
return got.post('http://localhost:3030/dbChange')
.catch(e => console.error('Error: lamassu-server not responding'))
}
app.use(morgan('dev'))
app.use(cookieParser())
app.use(register)
if (!devMode) app.use(authenticate)
app.use(bodyParser.json())
app.get('/api/totem', (req, res) => {
const name = req.query.name
if (!name) return res.status(400).send('Name is required')
return pairing.totem(hostname, name)
.then(totem => res.send(totem))
})
app.get('/api/accounts', (req, res) => {
accounts.selectedAccounts()
.then(accounts => res.json({accounts: accounts}))
})
app.get('/api/account/:account', (req, res) => {
accounts.getAccount(req.params.account)
.then(account => res.json(account))
})
app.post('/api/account', (req, res) => {
return accounts.updateAccount(req.body)
.then(account => res.json(account))
.then(() => dbNotify())
})
app.get('/api/config/:config', (req, res) =>
config.fetchConfigGroup(req.params.config).then(c => res.json(c)))
app.post('/api/config', (req, res) => {
config.saveConfigGroup(req.body)
.then(c => res.json(c))
.then(() => dbNotify())
})
app.get('/api/accounts/account/:account', (req, res) => {
accounts.getAccount(req.params.account)
.then(r => res.send(r))
})
app.get('/api/machines', (req, res) => {
machines.getMachines()
.then(r => res.send({machines: r}))
})
app.post('/api/machines', (req, res) => {
machines.setMachine(req.body)
.then(() => machines.getMachines())
.then(r => res.send({machines: r}))
.then(() => dbNotify())
})
app.get('/api/status', (req, res, next) => {
return Promise.all([server.status(), config.validateConfig()])
.then(([serverStatus, invalidConfigGroups]) => res.send({
server: serverStatus,
invalidConfigGroups
}))
.catch(next)
})
app.get('/api/transactions', (req, res, next) => {
return transactions.batch()
.then(r => res.send({transactions: r}))
.catch(next)
})
app.use((err, req, res, next) => {
console.error(err)
return res.status(500).send(err.message)
})
const options = {
key: fs.readFileSync(serverConfig.keyPath),
cert: fs.readFileSync(serverConfig.certPath)
}
app.use(serveStatic(path.resolve(__dirname, '..', 'public')))
function register (req, res, next) {
const otp = req.query.otp
if (!otp) return next()
return login.register(otp)
.then(r => {
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
if (!r.success) return res.status(401).send('Registration failed')
const cookieOpts = {
httpOnly: true,
secure: true
}
const token = r.token
req.token = token
res.cookie('token', token, cookieOpts)
next()
})
}
function authenticate (req, res, next) {
const token = req.token || req.cookies.token
return login.authenticate(token)
.then(success => {
if (!success) return res.status(401).send('Authentication failed')
next()
})
}
process.on('unhandledRejection', err => {
console.error(err.stack)
process.exit(1)
})
if (devMode) {
http.createServer(app).listen(8070, () => {
console.log('lamassu-admin-server listening on port 8070')
})
} else {
https.createServer(options, app).listen(443, () => {
console.log('lamassu-admin-server listening on port 443')
})
}

27
bin/lamassu-register Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env node
const login = require('../lib/admin/login')
const options = require('../lib/options')
const name = process.argv[2]
const domain = options.hostname
if (!domain) {
console.error('No hostname configured in lamassu.json')
process.exit(1)
}
if (!name) {
console.log('Usage: lamassu-register <username>')
process.exit(2)
}
login.generateOTP(name)
.then(otp => {
console.log(`https://${domain}?otp=${otp}`)
process.exit(0)
})
.catch(err => {
console.log('Error: %s', err)
process.exit(3)
})

2576
currencies.json Normal file

File diff suppressed because it is too large Load diff

253
languages.json Normal file
View file

@ -0,0 +1,253 @@
{
"attribute": {"name":0, "nativeName":1},
"rtl": {"ar":1,"dv":1,"fa":1,"ha":1,"he":1,"ks":1,"ku":1,"ps":1,"ur":1,"yi":1},
"lang": {
"aa":["Afar","Afar"],
"ab":["Abkhazian","Аҧсуа"],
"af":["Afrikaans","Afrikaans"],
"ak":["Akan","Akana"],
"am":["Amharic","አማርኛ"],
"an":["Aragonese","Aragonés"],
"ar":["Arabic","العربية"],
"as":["Assamese","অসমীয়া"],
"av":["Avar","Авар"],
"ay":["Aymara","Aymar"],
"az":["Azerbaijani","Azərbaycanca / آذربايجان"],
"ba":["Bashkir","Башҡорт"],
"be":["Belarusian","Беларуская"],
"bg":["Bulgarian","Български"],
"bh":["Bihari","भोजपुरी"],
"bi":["Bislama","Bislama"],
"bm":["Bambara","Bamanankan"],
"bn":["Bengali","বাংলা"],
"bo":["Tibetan","བོད་ཡིག / Bod skad"],
"br":["Breton","Brezhoneg"],
"bs":["Bosnian","Bosanski"],
"ca":["Catalan","Català"],
"ce":["Chechen","Нохчийн"],
"ch":["Chamorro","Chamoru"],
"co":["Corsican","Corsu"],
"cr":["Cree","Nehiyaw"],
"cs":["Czech","Česky"],
"cu":["Old Church Slavonic / Old Bulgarian","словѣньскъ / slověnĭskŭ"],
"cv":["Chuvash","Чăваш"],
"cy":["Welsh","Cymraeg"],
"da":["Danish","Dansk"],
"de":["German","Deutsch"],
"dv":["Divehi","ދިވެހިބަސް"],
"dz":["Dzongkha","ཇོང་ཁ"],
"ee":["Ewe","Ɛʋɛ"],
"el":["Greek","Ελληνικά"],
"en":["English","English"],
"eo":["Esperanto","Esperanto"],
"es":["Spanish","Español"],
"et":["Estonian","Eesti"],
"eu":["Basque","Euskara"],
"fa":["Persian","فارسی"],
"ff":["Peul","Fulfulde"],
"fi":["Finnish","Suomi"],
"fj":["Fijian","Na Vosa Vakaviti"],
"fo":["Faroese","Føroyskt"],
"fr":["French","Français"],
"fy":["West Frisian","Frysk"],
"ga":["Irish","Gaeilge"],
"gd":["Scottish Gaelic","Gàidhlig"],
"gl":["Galician","Galego"],
"gn":["Guarani","Avañe'ẽ"],
"gu":["Gujarati","ગુજરાતી"],
"gv":["Manx","Gaelg"],
"ha":["Hausa","هَوُسَ"],
"he":["Hebrew","עברית"],
"hi":["Hindi","हिन्दी"],
"ho":["Hiri Motu","Hiri Motu"],
"hr":["Croatian","Hrvatski"],
"ht":["Haitian","Krèyol ayisyen"],
"hu":["Hungarian","Magyar"],
"hy":["Armenian","Հայերեն"],
"hz":["Herero","Otsiherero"],
"ia":["Interlingua","Interlingua"],
"id":["Indonesian","Bahasa Indonesia"],
"ie":["Interlingue","Interlingue"],
"ig":["Igbo","Igbo"],
"ii":["Sichuan Yi","ꆇꉙ / 四川彝语"],
"ik":["Inupiak","Iñupiak"],
"io":["Ido","Ido"],
"is":["Icelandic","Íslenska"],
"it":["Italian","Italiano"],
"iu":["Inuktitut","ᐃᓄᒃᑎᑐᑦ"],
"ja":["Japanese","日本語"],
"jv":["Javanese","Basa Jawa"],
"ka":["Georgian","ქართული"],
"kg":["Kongo","KiKongo"],
"ki":["Kikuyu","Gĩkũyũ"],
"kj":["Kuanyama","Kuanyama"],
"kk":["Kazakh","Қазақша"],
"kl":["Greenlandic","Kalaallisut"],
"km":["Cambodian","ភាសាខ្មែរ"],
"kn":["Kannada","ಕನ್ನಡ"],
"ko":["Korean","한국어"],
"kr":["Kanuri","Kanuri"],
"ks":["Kashmiri","कश्मीरी / كشميري"],
"ku":["Kurdish","Kurdî / كوردی"],
"kv":["Komi","Коми"],
"kw":["Cornish","Kernewek"],
"ky":["Kirghiz","Kırgızca / Кыргызча"],
"la":["Latin","Latina"],
"lb":["Luxembourgish","Lëtzebuergesch"],
"lg":["Ganda","Luganda"],
"li":["Limburgian","Limburgs"],
"ln":["Lingala","Lingála"],
"lo":["Laotian","ລາວ / Pha xa lao"],
"lt":["Lithuanian","Lietuvių"],
"lv":["Latvian","Latviešu"],
"mg":["Malagasy","Malagasy"],
"mh":["Marshallese","Kajin Majel / Ebon"],
"mi":["Maori","Māori"],
"mk":["Macedonian","Македонски"],
"ml":["Malayalam","മലയാളം"],
"mn":["Mongolian","Монгол"],
"mo":["Moldovan","Moldovenească"],
"mr":["Marathi","मराठी"],
"ms":["Malay","Bahasa Melayu"],
"mt":["Maltese","bil-Malti"],
"my":["Burmese","Myanmasa"],
"na":["Nauruan","Dorerin Naoero"],
"nd":["North Ndebele","Sindebele"],
"ne":["Nepali","नेपाली"],
"ng":["Ndonga","Oshiwambo"],
"nl":["Dutch","Nederlands"],
"nn":["Norwegian Nynorsk","Norsk (nynorsk)"],
"no":["Norwegian","Norsk (bokmål / riksmål)"],
"nr":["South Ndebele","isiNdebele"],
"nv":["Navajo","Diné bizaad"],
"ny":["Chichewa","Chi-Chewa"],
"oc":["Occitan","Occitan"],
"oj":["Ojibwa","ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
"om":["Oromo","Oromoo"],
"or":["Oriya","ଓଡ଼ିଆ"],
"os":["Ossetian / Ossetic","Иронау"],
"pa":["Panjabi / Punjabi","ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
"pi":["Pali","Pāli / पाऴि"],
"pl":["Polish","Polski"],
"ps":["Pashto","پښتو"],
"pt":["Portuguese","Português"],
"qu":["Quechua","Runa Simi"],
"rm":["Raeto Romance","Rumantsch"],
"rn":["Kirundi","Kirundi"],
"ro":["Romanian","Română"],
"ru":["Russian","Русский"],
"rw":["Rwandi","Kinyarwandi"],
"sa":["Sanskrit","संस्कृतम्"],
"sc":["Sardinian","Sardu"],
"sd":["Sindhi","सिनधि"],
"se":["Northern Sami","Sámegiella"],
"sg":["Sango","Sängö"],
"sh":["Serbo-Croatian","Srpskohrvatski / Српскохрватски"],
"si":["Sinhalese","සිංහල"],
"sk":["Slovak","Slovenčina"],
"sl":["Slovenian","Slovenščina"],
"sm":["Samoan","Gagana Samoa"],
"sn":["Shona","chiShona"],
"so":["Somalia","Soomaaliga"],
"sq":["Albanian","Shqip"],
"sr":["Serbian","Српски"],
"ss":["Swati","SiSwati"],
"st":["Southern Sotho","Sesotho"],
"su":["Sundanese","Basa Sunda"],
"sv":["Swedish","Svenska"],
"sw":["Swahili","Kiswahili"],
"ta":["Tamil","தமிழ்"],
"te":["Telugu","తెలుగు"],
"tg":["Tajik","Тоҷикӣ"],
"th":["Thai","ไทย / Phasa Thai"],
"ti":["Tigrinya","ትግርኛ"],
"tk":["Turkmen","Туркмен / تركمن"],
"tl":["Tagalog / Filipino","Tagalog"],
"tn":["Tswana","Setswana"],
"to":["Tonga","Lea Faka-Tonga"],
"tr":["Turkish","Türkçe"],
"ts":["Tsonga","Xitsonga"],
"tt":["Tatar","Tatarça"],
"tw":["Twi","Twi"],
"ty":["Tahitian","Reo Mā`ohi"],
"ug":["Uyghur","Uyƣurqə / ئۇيغۇرچە"],
"uk":["Ukrainian","Українська"],
"ur":["Urdu","اردو"],
"uz":["Uzbek","Ўзбек"],
"ve":["Venda","Tshivenḓa"],
"vi":["Vietnamese","Tiếng Việt"],
"vo":["Volapük","Volapük"],
"wa":["Walloon","Walon"],
"wo":["Wolof","Wollof"],
"xh":["Xhosa","isiXhosa"],
"yi":["Yiddish","ייִדיש"],
"yo":["Yoruba","Yorùbá"],
"za":["Zhuang","Cuengh / Tôô / 壮语"],
"zh":["Chinese","中文"],
"zu":["Zulu","isiZulu"]
},
"supported": [
"en-US",
"en-CA",
"fr-QC",
"ach-UG",
"af-ZA",
"ar-SA",
"bg-BG",
"ca-ES",
"cs-CZ",
"cy-GB",
"de-DE",
"de-AT",
"de-CH",
"da-DK",
"el-GR",
"en-GB",
"en-AU",
"en-HK",
"en-IE",
"en-NZ",
"en-PR",
"es-ES",
"es-MX",
"et-EE",
"fi-FI",
"fr-FR",
"fr-CH",
"fur-IT",
"ga-IE",
"gd-GB",
"he-IL",
"hr-HR",
"hu-HU",
"id-ID",
"it-CH",
"it-IT",
"ja-JP",
"ko-KR",
"ky-KG",
"lt-LT",
"nb-NO",
"nl-BE",
"nl-NL",
"pt-PT",
"pt-BR",
"pl-PL",
"ro-RO",
"ru-RU",
"sco-GB",
"sh-HR",
"sk-SK",
"sl-SI",
"sr-SP",
"sv-SE",
"th-TH",
"tr-TR",
"uk-UA",
"vi-VN",
"zh-CN",
"zh-HK",
"zh-SG",
"zh-TW"
]
}

123
lib/admin/accounts.js Normal file
View file

@ -0,0 +1,123 @@
const R = require('ramda')
const fs = require('fs')
const path = require('path')
const options = require('../options')
const db = require('../db')
const accountRoot = options.pluginPath
const schemas = {}
function fetchSchemas () {
const files = fs.readdirSync(accountRoot)
files.forEach(file => {
if (file.indexOf('lamassu-') !== 0) return
try {
const schema = JSON.parse(fs.readFileSync(path.resolve(accountRoot, file, 'schema.json')))
schemas[schema.code] = schema
} catch (_) {
}
})
}
function fetchAccounts () {
return db.oneOrNone('select data from user_config where type=$1', ['accounts'])
.then(row => {
return row
? Promise.resolve(row.data.accounts)
: db.none('insert into user_config (type, data) values ($1, $2)', ['accounts', {accounts: []}])
.then(fetchAccounts)
})
}
function selectedAccounts () {
const mapAccount = v => v.fieldLocator.fieldType === 'account' &&
v.fieldValue.value
const mapSchema = code => schemas[code]
return db.oneOrNone('select data from user_config where type=$1', ['config'])
.then(row => row && row.data)
.then(data => {
if (!data) return []
const accountCodes = R.uniq(data.config.map(mapAccount)
.filter(R.identity))
return R.sortBy(R.prop('display'), accountCodes.map(mapSchema)
.filter(R.identity))
})
}
function fetchAccountSchema (account) {
return schemas[account]
}
function mergeAccount (oldAccount, newAccount) {
if (!newAccount) return oldAccount
const newFields = newAccount.fields
const updateWithData = oldField => {
const newField = R.find(r => r.code === oldField.code, newFields)
const newValue = newField ? newField.value : null
return R.assoc('value', newValue, oldField)
}
const updatedFields = oldAccount.fields.map(updateWithData)
return R.assoc('fields', updatedFields, oldAccount)
}
function getAccounts (accountCode) {
const schema = fetchAccountSchema(accountCode)
if (!schema) return Promise.reject(new Error('No schema for: ' + accountCode))
return fetchAccounts()
.then(accounts => {
if (R.isEmpty(accounts)) return [schema]
const account = R.find(r => r.code === accountCode, accounts)
const mergedAccount = mergeAccount(schema, account)
return updateAccounts(mergedAccount, accounts)
})
}
function getAccount (accountCode) {
return getAccounts(accountCode)
.then(accounts => R.find(r => r.code === accountCode, accounts))
}
function save (accounts) {
return db.none('update user_config set data=$1 where type=$2', [{accounts: accounts}, 'accounts'])
}
function updateAccounts (newAccount, accounts) {
const accountCode = newAccount.code
const isPresent = R.any(R.propEq('code', accountCode), accounts)
const updateAccount = r => r.code === accountCode
? newAccount
: r
return isPresent
? R.map(updateAccount, accounts)
: R.append(newAccount, accounts)
}
function updateAccount (account) {
return getAccounts(account.code)
.then(accounts => {
const merged = mergeAccount(R.find(R.propEq('code', account.code), accounts), account)
return save(updateAccounts(merged, accounts))
})
.then(() => getAccount(account.code))
}
fetchSchemas()
module.exports = {
selectedAccounts,
getAccount,
updateAccount
}

262
lib/admin/config.js Normal file
View file

@ -0,0 +1,262 @@
const pify = require('pify')
const fs = pify(require('fs'))
const path = require('path')
const R = require('ramda')
const currencies = require('../../currencies.json')
const languageRec = require('../../languages.json')
const db = require('../db')
const options = require('../options')
const configManager = require('../config-manager')
const machines = require('./machines')
function fetchSchema () {
const schemaPath = path.resolve(options.lamassuServerPath, 'lamassu-schema.json')
return fs.readFile(schemaPath)
.then(JSON.parse)
}
function dbFetchConfig () {
return db.oneOrNone('select data from user_config where type=$1', ['config'])
.then(row => row && row.data)
}
function allScopes (cryptoScopes, machineScopes) {
const scopes = []
cryptoScopes.forEach(c => {
machineScopes.forEach(m => scopes.push([c, m]))
})
return scopes
}
function allCryptoScopes (cryptos, cryptoScope) {
const cryptoScopes = []
if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global')
if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r))
return cryptoScopes
}
function allMachineScopes (machineList, machineScope) {
const machineScopes = []
if (machineScope === 'global' || machineScope === 'both') machineScopes.push('global')
if (machineScope === 'specific' || machineScope === 'both') machineList.forEach(r => machineScopes.push(r))
return machineScopes
}
function satisfiesRequire (config, cryptos, machineList, field, refFields) {
const fieldCode = field.code
const scopes = allScopes(
allCryptoScopes(cryptos, field.cryptoScope),
allMachineScopes(machineList, field.machineScope)
)
return scopes.every(scope => {
const isEnabled = () => refFields.some(refField => {
return isScopeEnabled(config, cryptos, machineList, refField, scope)
})
const isBlank = () => R.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config))
const isRequired = refFields.length === 0 || isEnabled()
return isRequired ? !isBlank() : true
})
}
function isScopeEnabled (config, cryptos, machineList, refField, scope) {
const [cryptoScope, machineScope] = scope
const candidateCryptoScopes = cryptoScope === 'global'
? allCryptoScopes(cryptos, refField.cryptoScope)
: [cryptoScope]
const candidateMachineScopes = machineScope === 'global'
? allMachineScopes(machineList, refField.machineScope)
: [ machineScope ]
const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes)
const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config)
const values = allRefCandidateScopes.map(getFallbackValue)
return values.some(r => r)
}
function getCryptos (config, machineList) {
const scopes = allScopes(['global'], allMachineScopes(machineList, 'both'))
const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config)
return scopes.reduce((acc, scope) => R.union(acc, scoped(scope)), [])
}
function getGroup (schema, fieldCode) {
return schema.groups.find(group => group.fields.find(R.equals(fieldCode)))
}
function getField (schema, group, fieldCode) {
if (!group) group = getGroup(schema, fieldCode)
const field = schema.fields.find(r => r.code === fieldCode)
return R.merge(R.pick(['cryptoScope', 'machineScope'], group), field)
}
const fetchMachines = () => machines.getMachines()
.then(machineList => machineList.map(r => r.deviceId))
function validateConfig () {
return Promise.all([fetchSchema(), dbFetchConfig(), fetchMachines()])
.then(([schema, configRec, machineList]) => {
const config = configRec ? configRec.config : []
const cryptos = getCryptos(config, machineList)
return schema.groups.filter(group => {
return group.fields.some(fieldCode => {
const field = getField(schema, group, fieldCode)
if (!field.fieldValidation.find(r => r.code === 'required')) return false
const refFields = (field.enabledIf || []).map(R.curry(getField)(schema, null))
return !satisfiesRequire(config, cryptos, machineList, field, refFields)
})
})
})
.then(arr => arr.map(r => r.code))
}
function fetchConfigGroup (code) {
const fieldLocatorCodeEq = R.pathEq(['fieldLocator', 'code'])
return Promise.all([fetchSchema(), fetchData(), dbFetchConfig(), fetchMachines()])
.then(([schema, data, config, machineList]) => {
const configValues = config ? config.config : []
const groupSchema = schema.groups.find(r => r.code === code)
if (!groupSchema) throw new Error('No such group schema: ' + code)
const schemaFields = groupSchema.fields
.map(R.curry(getField)(schema, groupSchema))
const candidateFields = [
schemaFields.map(R.prop('requiredIf')),
schemaFields.map(R.prop('enabledIf')),
groupSchema.fields
]
const configFields = R.uniq(R.flatten(candidateFields)).filter(R.identity)
const values = configFields
.reduce((acc, configField) => acc.concat(configValues.filter(fieldLocatorCodeEq(configField))), [])
groupSchema.fields = undefined
groupSchema.entries = schemaFields
return {
schema: groupSchema,
values: values,
selectedCryptos: getCryptos(config.config, machineList),
data: data
}
})
}
function massageCurrencies (currencies) {
const convert = r => ({
code: r['Alphabetic Code'],
display: r['Currency']
})
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
const mapped = R.map(convert, currencies)
const codeToRec = code => R.find(R.propEq('code', code), mapped)
const top5 = R.map(codeToRec, top5Codes)
const raw = R.uniqBy(R.prop('code'), R.concat(top5, mapped))
return raw.filter(r => r.code[0] !== 'X' && r.display.indexOf('(') === -1)
}
const mapLanguage = lang => {
const arr = lang.split('-')
const code = arr[0]
const country = arr[1]
const langNameArr = languageRec.lang[code]
if (!langNameArr) return null
const langName = langNameArr[0]
if (!country) return {code: lang, display: langName}
return {code: lang, display: `${langName} [${country}]`}
}
const supportedLanguages = languageRec.supported
const languages = supportedLanguages.map(mapLanguage).filter(r => r)
function fetchData () {
return machines.getMachines()
.then(machineList => ({
currencies: massageCurrencies(currencies),
cryptoCurrencies: [{crypto: 'BTC', display: 'Bitcoin'}, {crypto: 'ETH', display: 'Ethereum'}],
languages: languages,
accounts: [
{code: 'bitpay', display: 'Bitpay', class: 'ticker', cryptos: ['BTC']},
{code: 'kraken', display: 'Kraken', class: 'ticker', cryptos: ['BTC', 'ETH']},
{code: 'bitstamp', display: 'Bitstamp', class: 'ticker', cryptos: ['BTC']},
{code: 'coinbase', display: 'Coinbase', class: 'ticker', cryptos: ['BTC', 'ETH']},
{code: 'bitcoind', display: 'bitcoind', class: 'wallet', cryptos: ['BTC']},
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']},
{code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ['BTC', 'ETH']},
{code: 'mock-sms', display: 'Mock SMS', class: 'sms'},
{code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
{code: 'twilio', display: 'Twilio', class: 'sms'},
{code: 'mailjet', display: 'Mailjet', class: 'email'}
],
machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name}))
}))
}
function dbSaveConfig (config) {
return db.none('update user_config set data=$1 where type=$2', [config, 'config'])
}
function saveConfigGroup (results) {
return dbFetchConfig()
.then(config => {
return config
? Promise.resolve(config)
: db.none('insert into user_config (type, data) values ($1, $2)', ['config', {config: []}])
.then(dbFetchConfig)
})
.then(config => {
const oldValues = config.config
results.values.forEach(newValue => {
const oldValueIndex = oldValues
.findIndex(old => old.fieldLocator.code === newValue.fieldLocator.code &&
old.fieldLocator.fieldScope.crypto === newValue.fieldLocator.fieldScope.crypto &&
old.fieldLocator.fieldScope.machine === newValue.fieldLocator.fieldScope.machine
)
const existingValue = oldValueIndex > -1 &&
oldValues[oldValueIndex]
if (existingValue) {
// Delete value record
if (R.isNil(newValue.fieldValue)) {
oldValues.splice(oldValueIndex, 1)
return
}
existingValue.fieldValue = newValue.fieldValue
return
}
if (!R.isNil(newValue.fieldValue)) oldValues.push(newValue)
})
return dbSaveConfig(config)
.then(() => fetchConfigGroup(results.groupCode))
})
.catch(e => console.error(e.stack))
}
module.exports = {
fetchConfigGroup,
saveConfigGroup,
validateConfig
}

48
lib/admin/login.js Normal file
View file

@ -0,0 +1,48 @@
const crypto = require('crypto')
const db = require('../db')
function generateOTP (name) {
const otp = crypto.randomBytes(32).toString('hex')
const sql = 'insert into one_time_passes (token, name) values ($1, $2)'
return db.none(sql, [otp, name])
.then(() => otp)
}
function validateOTP (otp) {
const sql = `delete from one_time_passes
where token=$1
returning name, created < now() - interval '1 hour' as expired`
return db.one(sql, [otp])
.then(r => ({success: !r.expired, expired: r.expired, name: r.name}))
.catch(() => ({success: false, expired: false}))
}
function register (otp) {
return validateOTP(otp)
.then(r => {
if (!r.success) return r
const token = crypto.randomBytes(32).toString('hex')
const sql = 'insert into user_tokens (token, name) values ($1, $2)'
return db.none(sql, [token, r.name])
.then(() => ({success: true, token: token}))
})
.catch(() => ({success: false, expired: false}))
}
function authenticate (token) {
const sql = 'select token from user_tokens where token=$1'
return db.one(sql, [token]).then(() => true).catch(() => false)
}
module.exports = {
generateOTP,
register,
authenticate
}

26
lib/admin/machines.js Normal file
View file

@ -0,0 +1,26 @@
const db = require('../db')
function getMachines () {
return db.any('select * from devices where display=TRUE order by name')
.then(rr => rr.map(r => ({
deviceId: r.device_id,
name: r.name,
cashbox: r.cashbox,
cassette1: r.cassette1,
cassette2: r.cassette2,
paired: r.paired
})))
}
function resetCashOutBills (rec) {
const sql = 'update devices set cassette1=$1, cassette2=$2 where device_id=$3'
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
}
function setMachine (rec) {
switch (rec.action) {
case 'resetCashOutBills': return resetCashOutBills(rec)
}
}
module.exports = {getMachines, setMachine}

32
lib/admin/pairing.js Normal file
View file

@ -0,0 +1,32 @@
const fs = require('fs')
const pify = require('pify')
const readFile = pify(fs.readFile)
const crypto = require('crypto')
const options = require('../options')
const db = require('../db')
function unpair (deviceId) {
const sql = 'update devices set paired=FALSE where device_id=$1'
return db.none(sql, [deviceId])
}
function totem (hostname, 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(hostname)])
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
return db.none(sql, [hexToken, caHexToken, name])
.then(() => buf.toString('base64'))
})
}
module.exports = {totem, unpair}

26
lib/admin/server.js Normal file
View file

@ -0,0 +1,26 @@
const moment = require('moment')
const db = require('../db')
const CONSIDERED_UP = 30000
function status () {
const sql = `select extract(epoch from (now() - created)) as age
from server_events
where event_type=$1
order by created desc
limit 1`
return db.oneOrNone(sql, ['ping'])
.then(row => {
if (!row) return {up: false, lastPing: null}
const age = moment.duration(row.age, 'seconds')
const up = age.asMilliseconds() < CONSIDERED_UP
const lastPing = age.humanize()
return {up, lastPing}
})
}
module.exports = {status}

25
lib/admin/transactions.js Normal file
View file

@ -0,0 +1,25 @@
const _ = require('lodash/fp')
const db = require('../db')
const NUM_RESULTS = 20
function batch () {
const camelize = _.mapKeys(_.camelCase)
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.take(NUM_RESULTS), _.map(camelize))
const cashInSql = `select 'cashIn' as tx_class, devices.name as machine_name, cash_in_txs.*
from cash_in_txs, devices
where devices.device_id=cash_in_txs.device_id
order by created desc limit $1`
const cashOutSql = `select 'cashOut' as tx_class, devices.name as machine_name, cash_out_txs.*
from cash_out_txs, devices
where devices.device_id=cash_out_txs.device_id
order by created desc limit $1`
return Promise.all([db.any(cashInSql, [NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS])])
.then(packager)
}
module.exports = {batch}

View file

@ -1,10 +1,11 @@
const R = require('ramda')
const _ = require('lodash/fp')
module.exports = {
unscoped,
cryptoScoped,
machineScoped,
scoped
scoped,
scopedValue
}
function matchesValue (crypto, machine, instance) {
@ -13,7 +14,7 @@ function matchesValue (crypto, machine, instance) {
}
function permutations (crypto, machine) {
return R.uniq([
return _.uniq([
[crypto, machine],
[crypto, 'global'],
['global', machine],
@ -22,21 +23,26 @@ function permutations (crypto, machine) {
}
function fallbackValue (crypto, machine, instances) {
const notNil = R.pipe(R.isNil, R.not)
const pickValue = arr => R.find(instance => matchesValue(arr[0], arr[1], instance), instances)
const fallbackRec = R.find(notNil, R.map(pickValue, permutations(crypto, machine)))
const notNil = _.negate(_.isNil)
const pickValue = arr => _.find(instance => matchesValue(arr[0], arr[1], instance), instances)
const fallbackRec = _.find(notNil, _.map(pickValue, permutations(crypto, machine)))
return fallbackRec && fallbackRec.fieldValue.value
}
function scopedValue (crypto, machine, fieldCode, config) {
const allScopes = config.filter(_.pathEq(['fieldLocator', 'code'], fieldCode))
return fallbackValue(crypto, machine, allScopes)
}
function generalScoped (crypto, machine, config) {
const scopedValue = (key, instances) =>
fallbackValue(crypto, machine, instances)
const localScopedValue = key =>
scopedValue(crypto, machine, key, config)
const allScopes = key => config.filter(R.pathEq(['fieldLocator', 'code'], key))
const keys = R.uniq(R.map(r => r.fieldLocator.code, config))
const keyedValues = keys.map(key => scopedValue(key, allScopes(key)))
const keys = _.uniq(_.map(r => r.fieldLocator.code, config))
const keyedValues = keys.map(localScopedValue)
return R.zipObj(keys, keyedValues)
return _.zipObject(keys, keyedValues)
}
function machineScoped (machine, config) {

View file

@ -397,6 +397,7 @@ function executeTrades () {
const fiatCode = config.fiatCurrency
const cryptoCodes = config.cryptoCurrencies
console.log('DEBUG99: %j', config)
return cryptoCodes.map(cryptoCode => ({fiatCode, cryptoCode}))
})

View file

@ -34,7 +34,12 @@
"ramda": "^0.22.1",
"reoccur": "^1.0.0",
"uuid": "^3.0.0",
"winston": "^2.3.0"
"winston": "^2.3.0",
"cookie-parser": "^1.4.3",
"got": "^6.6.3",
"lodash": "^4.17.2",
"moment": "^2.17.0",
"serve-static": "^1.11.1"
},
"repository": {
"type": "git",
@ -43,6 +48,9 @@
"bin": {
"lamassu-server": "./bin/lamassu-server",
"lamassu-migrate": "./bin/lamassu-migrate",
"lamassu-register": "./bin/lamassu-register",
"lamassu-domain": "./bin/lamassu-domain",
"lamassu-admin-server": "./bin/lamassu-admin-server",
"hkdf": "./bin/hkdf"
},
"scripts": {},

24
tools/currencies.js Normal file
View file

@ -0,0 +1,24 @@
// Pull latest from: http://www.currency-iso.org/en/home/tables/table-a1.html
// Convert to JSON at: http://www.csvjson.com/csv2json
const R = require('ramda')
const currencies = require('../currencies.json')
function goodCurrency (currency) {
const code = currency.code
return code.length === 3 && code[0] !== 'X'
}
function simplify (currency) {
return {
code: currency['Alphabetic Code'],
display: currency['Currency']
}
}
function toElmItem (currency) {
return `{ code = "${currency.code}"
, display = "${currency.display}"
, searchWords = []
}`
}

40
tools/modify.js Normal file
View file

@ -0,0 +1,40 @@
'use strict'
const R = require('ramda')
const db = require('../db')
function pp (o) {
console.log(require('util').inspect(o, {depth: null, colors: true}))
}
function dbFetchConfig () {
return db.oneOrNone('select data from user_config where type=$1', ['config'])
.then(row => row && row.data)
}
dbFetchConfig()
.then(c => {
const groups = c.groups
.filter(g => g.code !== 'fiat')
.map(g => {
if (g.code === 'currencies') {
const values = g.values.filter(v => v.fieldLocator.code !== 'cryptoCurrencies')
return R.assoc('values', values, g)
}
return g
})
return {groups: groups}
})
.then(config => {
pp(config)
return db.none('update user_config set data=$1 where type=$2', [config, 'config'])
})
.then(() => {
process.exit(0)
})
.catch(e => {
console.log(e)
process.exit(1)
})

22
tools/show.js Normal file
View file

@ -0,0 +1,22 @@
'use strict'
const db = require('../lib/db')
function pp (o) {
console.log(require('util').inspect(o, {depth: null, colors: true}))
}
function dbFetchConfig () {
return db.oneOrNone('select data from user_config where type=$1', ['config'])
.then(row => row && row.data)
}
dbFetchConfig()
.then(config => {
pp(config)
process.exit(0)
})
.catch(e => {
console.log(e)
process.exit(1)
})

518
yarn.lock

File diff suppressed because it is too large Load diff