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 = {
|
module.exports = {
|
||||||
unscoped,
|
unscoped,
|
||||||
cryptoScoped,
|
cryptoScoped,
|
||||||
machineScoped,
|
machineScoped,
|
||||||
scoped
|
scoped,
|
||||||
|
scopedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesValue (crypto, machine, instance) {
|
function matchesValue (crypto, machine, instance) {
|
||||||
|
|
@ -13,7 +14,7 @@ function matchesValue (crypto, machine, instance) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function permutations (crypto, machine) {
|
function permutations (crypto, machine) {
|
||||||
return R.uniq([
|
return _.uniq([
|
||||||
[crypto, machine],
|
[crypto, machine],
|
||||||
[crypto, 'global'],
|
[crypto, 'global'],
|
||||||
['global', machine],
|
['global', machine],
|
||||||
|
|
@ -22,21 +23,26 @@ function permutations (crypto, machine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fallbackValue (crypto, machine, instances) {
|
function fallbackValue (crypto, machine, instances) {
|
||||||
const notNil = R.pipe(R.isNil, R.not)
|
const notNil = _.negate(_.isNil)
|
||||||
const pickValue = arr => R.find(instance => matchesValue(arr[0], arr[1], instance), instances)
|
const pickValue = arr => _.find(instance => matchesValue(arr[0], arr[1], instance), instances)
|
||||||
const fallbackRec = R.find(notNil, R.map(pickValue, permutations(crypto, machine)))
|
const fallbackRec = _.find(notNil, _.map(pickValue, permutations(crypto, machine)))
|
||||||
return fallbackRec && fallbackRec.fieldValue.value
|
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) {
|
function generalScoped (crypto, machine, config) {
|
||||||
const scopedValue = (key, instances) =>
|
const localScopedValue = key =>
|
||||||
fallbackValue(crypto, machine, instances)
|
scopedValue(crypto, machine, key, config)
|
||||||
|
|
||||||
const allScopes = key => config.filter(R.pathEq(['fieldLocator', 'code'], key))
|
const keys = _.uniq(_.map(r => r.fieldLocator.code, config))
|
||||||
const keys = R.uniq(R.map(r => r.fieldLocator.code, config))
|
const keyedValues = keys.map(localScopedValue)
|
||||||
const keyedValues = keys.map(key => scopedValue(key, allScopes(key)))
|
|
||||||
|
|
||||||
return R.zipObj(keys, keyedValues)
|
return _.zipObject(keys, keyedValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
function machineScoped (machine, config) {
|
function machineScoped (machine, config) {
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,7 @@ function executeTrades () {
|
||||||
const fiatCode = config.fiatCurrency
|
const fiatCode = config.fiatCurrency
|
||||||
const cryptoCodes = config.cryptoCurrencies
|
const cryptoCodes = config.cryptoCurrencies
|
||||||
|
|
||||||
|
console.log('DEBUG99: %j', config)
|
||||||
return cryptoCodes.map(cryptoCode => ({fiatCode, cryptoCode}))
|
return cryptoCodes.map(cryptoCode => ({fiatCode, cryptoCode}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -34,7 +34,12 @@
|
||||||
"ramda": "^0.22.1",
|
"ramda": "^0.22.1",
|
||||||
"reoccur": "^1.0.0",
|
"reoccur": "^1.0.0",
|
||||||
"uuid": "^3.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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -43,6 +48,9 @@
|
||||||
"bin": {
|
"bin": {
|
||||||
"lamassu-server": "./bin/lamassu-server",
|
"lamassu-server": "./bin/lamassu-server",
|
||||||
"lamassu-migrate": "./bin/lamassu-migrate",
|
"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"
|
"hkdf": "./bin/hkdf"
|
||||||
},
|
},
|
||||||
"scripts": {},
|
"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