moved l-a-s in here
This commit is contained in:
parent
1e3e55e362
commit
836ab07776
18 changed files with 3946 additions and 281 deletions
184
bin/lamassu-admin-server
Executable file
184
bin/lamassu-admin-server
Executable 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
27
bin/lamassu-register
Executable 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
2576
currencies.json
Normal file
File diff suppressed because it is too large
Load diff
253
languages.json
Normal file
253
languages.json
Normal 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
123
lib/admin/accounts.js
Normal 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
262
lib/admin/config.js
Normal 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
48
lib/admin/login.js
Normal 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
26
lib/admin/machines.js
Normal 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
32
lib/admin/pairing.js
Normal 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
26
lib/admin/server.js
Normal 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
25
lib/admin/transactions.js
Normal 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}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}))
|
||||
})
|
||||
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -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
24
tools/currencies.js
Normal 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
40
tools/modify.js
Normal 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
22
tools/show.js
Normal 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)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue