chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View file

@ -0,0 +1,124 @@
const fs = require('fs')
const compression = require('compression')
const path = require('path')
const express = require('express')
const https = require('https')
const serveStatic = require('serve-static')
const helmet = require('helmet')
const nocache = require('nocache')
const cookieParser = require('cookie-parser')
const { ApolloServer } = require('@apollo/server')
const { expressMiddleware } = require('@apollo/server/express4')
const { ApolloServerPluginLandingPageDisabled } = require('@apollo/server/plugin/disabled')
const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default')
const { mergeResolvers } = require('@graphql-tools/merge')
const { makeExecutableSchema } = require('@graphql-tools/schema')
require('../environment-helper')
const logger = require('../logger')
const exchange = require('../exchange')
const { authDirectiveTransformer } = require('./graphql/directives')
const { typeDefs, resolvers } = require('./graphql/schema')
const findOperatorId = require('../middlewares/operatorId')
const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants')
const { session, cleanUserSessions, buildApolloContext } = require('./middlewares')
const devMode = require('minimist')(process.argv.slice(2)).dev
const HOSTNAME = process.env.HOSTNAME
const KEY_PATH = process.env.KEY_PATH
const CERT_PATH = process.env.CERT_PATH
const CA_PATH = process.env.CA_PATH
const ID_PHOTO_CARD_DIR = process.env.ID_PHOTO_CARD_DIR
const FRONT_CAMERA_DIR = process.env.FRONT_CAMERA_DIR
const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR
if (!HOSTNAME) {
logger.error('No hostname specified.')
process.exit(1)
}
const loadRoutes = async () => {
const app = express()
app.use(helmet())
app.use(compression())
app.use(nocache())
app.use(cookieParser())
app.use(express.json())
app.use(express.urlencoded({ extended: true })) // support encoded bodies
app.use(express.static(path.resolve(__dirname, '..', '..', 'public')))
app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL))
app.use(findOperatorId)
app.use(session)
// Dynamic import for graphql-upload since it's not a CommonJS module
const { default: graphqlUploadExpress } = await import('graphql-upload/graphqlUploadExpress.mjs')
const { default: GraphQLUpload } = await import('graphql-upload/GraphQLUpload.mjs')
app.use(graphqlUploadExpress())
const schema = makeExecutableSchema({
typeDefs,
resolvers: mergeResolvers(resolvers, { Upload: GraphQLUpload }),
})
const schemaWithDirectives = authDirectiveTransformer(schema)
const apolloServer = new ApolloServer({
schema: schemaWithDirectives,
csrfPrevention: false,
introspection: false,
formatError: (formattedError, error) => {
logger.error(error, JSON.stringify(error?.extensions || {}))
return formattedError
},
plugins: [
devMode
? ApolloServerPluginLandingPageLocalDefault()
: ApolloServerPluginLandingPageDisabled()
]
})
await apolloServer.start();
app.use(
'/graphql',
express.json(),
expressMiddleware(apolloServer, {
context: async ({ req, res }) => buildApolloContext({ req, res })
})
);
app.use('/id-card-photo', serveStatic(ID_PHOTO_CARD_DIR, { index: false }))
app.use('/front-camera-photo', serveStatic(FRONT_CAMERA_DIR, { index: false }))
app.use('/operator-data', serveStatic(OPERATOR_DATA_DIR, { index: false }))
// Everything not on graphql or api/register is redirected to the front-end
app.get('*', (req, res) => res.sendFile(path.resolve(__dirname, '..', '..', 'public', 'index.html')))
return app
}
const certOptions = {
key: fs.readFileSync(KEY_PATH),
cert: fs.readFileSync(CERT_PATH),
ca: fs.readFileSync(CA_PATH)
}
async function run () {
const app = await loadRoutes()
const serverPort = devMode ? 8070 : 443
const serverLog = `lamassu-admin-server listening on port ${serverPort}`
// cache markets on startup
exchange.getMarkets().catch(console.error)
const webServer = https.createServer(certOptions, app)
webServer.listen(serverPort, () => logger.info(serverLog))
}
module.exports = { run }

View file

@ -0,0 +1,74 @@
const { COINS, ALL_CRYPTOS } = require('@lamassu/coins')
const _ = require('lodash/fp')
const { ALL } = require('../../plugins/common/ccxt')
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
const TICKER = 'ticker'
const WALLET = 'wallet'
const LAYER_2 = 'layer2'
const EXCHANGE = 'exchange'
const SMS = 'sms'
const ID_VERIFIER = 'idVerifier'
const EMAIL = 'email'
const ZERO_CONF = 'zeroConf'
const WALLET_SCORING = 'wallet_scoring'
const COMPLIANCE = 'compliance'
const ALL_ACCOUNTS = [
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
{ code: 'bitfinex', display: 'Bitfinex', class: EXCHANGE, cryptos: bitfinex.CRYPTO },
{ code: 'binance', display: 'Binance', class: TICKER, cryptos: binance.CRYPTO },
{ code: 'binanceus', display: 'Binance.us', class: TICKER, cryptos: binanceus.CRYPTO },
{ code: 'cex', display: 'CEX.IO', class: TICKER, cryptos: cex.CRYPTO },
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
{ code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.CRYPTO },
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: bitstamp.CRYPTO },
{ code: 'itbit', display: 'itBit', class: TICKER, cryptos: itbit.CRYPTO },
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
{ code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT, USDC] },
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] },
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
{ code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] },
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO },
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO },
{ code: 'binance', display: 'Binance', class: EXCHANGE, cryptos: binance.CRYPTO },
{ code: 'binanceus', display: 'Binance.us', class: EXCHANGE, cryptos: binanceus.CRYPTO },
{ code: 'cex', display: 'CEX.IO', class: EXCHANGE, cryptos: cex.CRYPTO },
{ code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS },
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
{ code: 'twilio', display: 'Twilio', class: SMS },
{ code: 'telnyx', display: 'Telnyx', class: SMS },
{ code: 'vonage', display: 'Vonage', class: SMS },
{ code: 'inforu', display: 'InforU', class: SMS },
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX] },
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC] },
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
{ code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
{ code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
]
const flags = require('minimist')(process.argv.slice(2))
const devMode = flags.dev || flags.lamassuDev
const ACCOUNT_LIST = devMode ? ALL_ACCOUNTS : _.filter(it => !it.dev)(ALL_ACCOUNTS)
module.exports = { ACCOUNT_LIST }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,255 @@
{
"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",
"hy-AM",
"id-ID",
"it-CH",
"it-IT",
"ja-JP",
"ka-GE",
"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"
]
}

View file

@ -0,0 +1,52 @@
const _ = require('lodash/fp')
const { CRYPTO_CURRENCIES } = require('@lamassu/coins')
const { ACCOUNT_LIST: accounts } = require('./accounts')
const countries = require('./data/countries.json')
const currenciesRec = require('./data/currencies.json')
const languageRec = require('./data/languages.json')
function massageCurrencies (currencies) {
const convert = r => ({
code: r['Alphabetic Code'],
display: r['Currency']
})
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
const mapped = _.map(convert, currencies)
const codeToRec = code => _.find(_.matchesProperty('code', code), mapped)
const top5 = _.map(codeToRec, top5Codes)
const raw = _.uniqBy(_.get('code'), _.concat(top5, mapped))
return raw.filter(r => r.code !== '' && 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 massageCryptos = cryptos => {
const betaList = ['LN']
const convert = crypto => ({
code: crypto['cryptoCode'],
display: crypto['display'],
codeDisplay: crypto['cryptoCodeDisplay'] ?? crypto['cryptoCode'],
isBeta: betaList.includes(crypto.cryptoCode)
})
return _.map(convert, cryptos)
}
const supportedLanguages = languageRec.supported
const languages = supportedLanguages.map(mapLanguage).filter(r => r)
const currencies = massageCurrencies(currenciesRec)
const coins = massageCryptos(CRYPTO_CURRENCIES)
module.exports = { coins, accounts, countries, currencies, languages }

View file

@ -0,0 +1,44 @@
const db = require('../db')
const cashInTx = require('../cash-in/cash-in-tx')
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
function transaction () {
const sql = `SELECT DISTINCT * FROM (
SELECT 'type' AS type, NULL AS label, 'Cash In' AS value UNION
SELECT 'type' AS type, NULL AS label, 'Cash Out' AS value UNION
SELECT 'machine' AS type, name AS label, d.device_id AS value FROM devices d INNER JOIN cash_in_txs t ON d.device_id = t.device_id UNION
SELECT 'machine' AS type, name AS label, d.device_id AS value FROM devices d INNER JOIN cash_out_txs t ON d.device_id = t.device_id UNION
SELECT 'customer' AS type, NULL AS label, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
FROM customers c INNER JOIN cash_in_txs t ON c.id = t.customer_id
WHERE c.id_card_data::json->>'firstName' IS NOT NULL or c.id_card_data::json->>'lastName' IS NOT NULL UNION
SELECT 'customer' AS type, NULL AS label, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value
FROM customers c INNER JOIN cash_out_txs t ON c.id = t.customer_id
WHERE c.id_card_data::json->>'firstName' IS NOT NULL or c.id_card_data::json->>'lastName' IS NOT NULL UNION
SELECT 'fiat' AS type, NULL AS label, fiat_code AS value FROM cash_in_txs UNION
SELECT 'fiat' AS type, NULL AS label, fiat_code AS value FROM cash_out_txs UNION
SELECT 'crypto' AS type, NULL AS label, crypto_code AS value FROM cash_in_txs UNION
SELECT 'crypto' AS type, NULL AS label, crypto_code AS value FROM cash_out_txs UNION
SELECT 'address' AS type, NULL AS label, to_address AS value FROM cash_in_txs UNION
SELECT 'address' AS type, NULL AS label, to_address AS value FROM cash_out_txs UNION
SELECT 'status' AS type, NULL AS label, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
SELECT 'status' AS type, NULL AS label, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs UNION
SELECT 'sweep status' AS type, NULL AS label, CASE WHEN swept THEN 'Swept' WHEN NOT swept THEN 'Unswept' END AS value FROM cash_out_txs
) f`
return db.any(sql)
}
function customer () {
const sql = `SELECT DISTINCT * FROM (
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION
SELECT 'name' AS type, id_card_data::json->>'firstName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NULL UNION
SELECT 'name' AS type, id_card_data::json->>'lastName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION
SELECT 'name' AS type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION
SELECT 'address' as type, id_card_data::json->>'address' AS value FROM customers WHERE id_card_data::json->>'address' IS NOT NULL UNION
SELECT 'id' AS type, id_card_data::json->>'documentNumber' AS value FROM customers WHERE id_card_data::json->>'documentNumber' IS NOT NULL
) f`
return db.any(sql)
}
module.exports = { transaction, customer }

View file

@ -0,0 +1,49 @@
const _ = require('lodash/fp')
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils')
const { defaultFieldResolver } = require('graphql')
const { AuthenticationError } = require('../errors')
function authDirectiveTransformer(schema, directiveName = 'auth') {
return mapSchema(schema, {
// For object types
[MapperKind.OBJECT_TYPE]: (objectType) => {
const directive = getDirective(schema, objectType, directiveName)?.[0]
if (directive) {
const requiredAuthRole = directive.requires
objectType._requiredAuthRole = requiredAuthRole
}
return objectType
},
// For field definitions
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0]
if (directive) {
const requiredAuthRole = directive.requires
fieldConfig._requiredAuthRole = requiredAuthRole
}
// Get the parent object type
const objectType = schema.getType(typeName)
// Apply auth check to the field's resolver
const { resolve = defaultFieldResolver } = fieldConfig
fieldConfig.resolve = function (root, args, context, info) {
const requiredRoles = fieldConfig._requiredAuthRole || objectType._requiredAuthRole
if (!requiredRoles) return resolve.apply(this, [root, args, context, info])
const user = context.req.session.user
if (!user || !_.includes(_.upperCase(user.role), requiredRoles)) {
throw new AuthenticationError('You do not have permission to access this resource!')
}
return resolve.apply(this, [root, args, context, info])
}
return fieldConfig
}
})
}
module.exports = authDirectiveTransformer

View file

@ -0,0 +1,3 @@
const authDirectiveTransformer = require('./auth')
module.exports = { authDirectiveTransformer }

View file

@ -0,0 +1,71 @@
const { GraphQLError } = require('graphql')
const { ApolloServerErrorCode } = require('@apollo/server/errors')
class AuthenticationError extends GraphQLError {
constructor() {
super('Authentication failed', {
extensions: {
code: 'UNAUTHENTICATED'
}
})
}
}
class InvalidCredentialsError extends GraphQLError {
constructor() {
super('Invalid credentials', {
extensions: {
code: 'INVALID_CREDENTIALS'
}
})
}
}
class UserAlreadyExistsError extends GraphQLError {
constructor() {
super('User already exists', {
extensions: {
code: 'USER_ALREADY_EXISTS'
}
})
}
}
class InvalidTwoFactorError extends GraphQLError {
constructor() {
super('Invalid two-factor code', {
extensions: {
code: 'INVALID_TWO_FACTOR_CODE'
}
})
}
}
class InvalidUrlError extends GraphQLError {
constructor() {
super('Invalid URL token', {
extensions: {
code: 'INVALID_URL_TOKEN'
}
})
}
}
class UserInputError extends GraphQLError {
constructor() {
super('User input error', {
extensions: {
code: ApolloServerErrorCode.BAD_USER_INPUT
}
})
}
}
module.exports = {
AuthenticationError,
InvalidCredentialsError,
UserAlreadyExistsError,
InvalidTwoFactorError,
InvalidUrlError,
UserInputError
}

View file

@ -0,0 +1,175 @@
const simpleWebauthn = require('@simplewebauthn/server')
const base64url = require('base64url')
const _ = require('lodash/fp')
const userManagement = require('../userManagement')
const credentials = require('../../../../hardware-credentials')
const T = require('../../../../time')
const users = require('../../../../users')
const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID: options.domain
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
}
return opts
})
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
return userManagement.authenticateUser(options.username, options.password).then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,174 @@
const simpleWebauthn = require('@simplewebauthn/server')
const base64url = require('base64url')
const _ = require('lodash/fp')
const credentials = require('../../../../hardware-credentials')
const T = require('../../../../time')
const users = require('../../../../users')
const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day
const generateAttestationOptions = (session, options) => {
return users.getUserById(options.userId).then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
}).then(([userDevices, user]) => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: user.username,
userID: user.id,
timeout: 60000,
attestationType: 'indirect',
excludeCredentials: userDevices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = (session, options) => {
return users.getUserByUsername(options.username).then(user => {
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID: options.domain
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
}
return opts
})
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return false
}
const {
counter,
credentialPublicKey,
credentialID
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
if (!existingDevice) {
const newDevice = {
counter,
credentialPublicKey,
credentialID
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
return users.getUserByUsername(options.username).then(user => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
context.req.session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return credentials.updateHardwareCredential(dbAuthenticator)
.then(() => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,181 @@
const simpleWebauthn = require('@simplewebauthn/server')
const base64url = require('base64url')
const _ = require('lodash/fp')
const credentials = require('../../../../hardware-credentials')
const T = require('../../../../time')
const users = require('../../../../users')
const devMode = require('minimist')(process.argv.slice(2)).dev
const REMEMBER_ME_AGE = 90 * T.day
const generateAttestationOptions = (session, options) => {
return credentials.getHardwareCredentials().then(devices => {
const opts = simpleWebauthn.generateAttestationOptions({
rpName: 'Lamassu',
rpID: options.domain,
userName: `Usernameless user created at ${new Date().toISOString()}`,
userID: options.userId,
timeout: 60000,
attestationType: 'direct',
excludeCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
userVerification: 'discouraged',
requireResidentKey: false
}
})
session.webauthn = {
attestation: {
challenge: opts.challenge
}
}
return opts
})
}
const generateAssertionOptions = (session, options) => {
return credentials.getHardwareCredentials().then(devices => {
const opts = simpleWebauthn.generateAssertionOptions({
timeout: 60000,
allowCredentials: devices.map(dev => ({
id: dev.data.credentialID,
type: 'public-key',
transports: ['usb', 'ble', 'nfc', 'internal']
})),
userVerification: 'discouraged',
rpID: options.domain
})
session.webauthn = {
assertion: {
challenge: opts.challenge
}
}
return opts
})
}
const validateAttestation = (session, options) => {
const webauthnData = session.webauthn.attestation
const expectedChallenge = webauthnData.challenge
return Promise.all([
users.getUserById(options.userId),
simpleWebauthn.verifyAttestationResponse({
credential: options.attestationResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain
})
])
.then(([user, verification]) => {
const { verified, attestationInfo } = verification
if (!(verified || attestationInfo)) {
session.webauthn = null
return verified
}
const {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject
} = attestationInfo
return credentials.getHardwareCredentialsByUserId(user.id)
.then(userDevices => {
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
if (!existingDevice) {
const newDevice = {
fmt,
counter,
aaguid,
credentialPublicKey,
credentialID,
credentialType,
userVerified,
attestationObject
}
credentials.createHardwareCredential(user.id, newDevice)
}
session.webauthn = null
return verified
})
})
}
const validateAssertion = (session, options) => {
const expectedChallenge = session.webauthn.assertion.challenge
return credentials.getHardwareCredentials().then(devices => {
const dbAuthenticator = _.find(dev => {
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
}, devices)
if (!dbAuthenticator.data) {
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
}
const convertedAuthenticator = _.merge(
dbAuthenticator.data,
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
)
let verification
try {
verification = simpleWebauthn.verifyAssertionResponse({
credential: options.assertionResponse,
expectedChallenge: `${expectedChallenge}`,
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
expectedRPID: options.domain,
authenticator: convertedAuthenticator
})
} catch (err) {
console.error(err)
return false
}
const { verified, assertionInfo } = verification
if (!verified) {
session.webauthn = null
return false
}
dbAuthenticator.data.counter = assertionInfo.newCounter
return Promise.all([
credentials.updateHardwareCredential(dbAuthenticator),
users.getUserById(dbAuthenticator.user_id)
])
.then(([_, user]) => {
const finalUser = { id: user.id, username: user.username, role: user.role }
session.user = finalUser
session.cookie.maxAge = REMEMBER_ME_AGE
session.webauthn = null
return verified
})
})
}
module.exports = {
generateAttestationOptions,
generateAssertionOptions,
validateAttestation,
validateAssertion
}

View file

@ -0,0 +1,17 @@
const FIDO2FA = require('./FIDO2FAStrategy')
const FIDOPasswordless = require('./FIDOPasswordlessStrategy')
const FIDOUsernameless = require('./FIDOUsernamelessStrategy')
const STRATEGIES = {
FIDO2FA,
FIDOPasswordless,
FIDOUsernameless
}
// FIDO2FA, FIDOPasswordless or FIDOUsernameless
const CHOSEN_STRATEGY = 'FIDO2FA'
module.exports = {
CHOSEN_STRATEGY,
strategy: STRATEGIES[CHOSEN_STRATEGY]
}

View file

@ -0,0 +1,271 @@
const otplib = require('otplib')
const argon2 = require('argon2')
const _ = require('lodash/fp')
const constants = require('../../../constants')
const authTokens = require('../../../auth-tokens')
const loginHelper = require('../../services/login')
const T = require('../../../time')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const authErrors = require('../errors')
const credentials = require('../../../hardware-credentials')
const REMEMBER_ME_AGE = 90 * T.day
const authenticateUser = (username, password) => {
return users.getUserByUsername(username)
.then(user => {
const hashedPassword = user.password
if (!hashedPassword || !user.enabled) throw new authErrors.InvalidCredentialsError()
return Promise.all([argon2.verify(hashedPassword, password), hashedPassword])
})
.then(([isMatch, hashedPassword]) => {
if (!isMatch) throw new authErrors.InvalidCredentialsError()
return loginHelper.validateUser(username, hashedPassword)
})
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return user
})
}
const destroySessionIfSameUser = (context, user) => {
const sessionUser = getUserFromCookie(context)
if (sessionUser && user.id === sessionUser.id) { context.req.session.destroy() }
}
const destroySessionIfBeingUsed = (sessID, context) => {
if (sessID === context.req.session.id) {
context.req.session.destroy()
}
}
const getUserFromCookie = context => {
return context.req.session.user
}
const getLamassuCookie = context => {
return context.req.cookies && context.req.cookies.lamassu_sid
}
const initializeSession = (context, user, rememberMe) => {
const finalUser = { id: user.id, username: user.username, role: user.role }
context.req.session.user = finalUser
if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE
}
const executeProtectedAction = (code, id, context, action) => {
return users.getUserById(id)
.then(user => {
if (user.role !== 'superuser') {
return action()
}
return confirm2FA(code, context)
.then(() => action())
})
}
const getUserData = context => {
const lidCookie = getLamassuCookie(context)
if (!lidCookie) return
const user = getUserFromCookie(context)
return user
}
const get2FASecret = (username, password) => {
return authenticateUser(username, password)
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
return Promise.all([users.saveTemp2FASecret(user.id, secret), secret, otpauth])
})
.then(([_, secret, otpauth]) => {
return { secret, otpauth }
})
}
const confirm2FA = (token, context) => {
const requestingUser = getUserFromCookie(context)
if (!requestingUser) throw new authErrors.InvalidCredentialsError()
return users.getUserById(requestingUser.id).then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token, secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
return true
})
}
const validateRegisterLink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateUserRegistrationToken(token)
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { username: r.username, role: r.role }
})
}
const validateResetPasswordLink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateAuthToken(token, 'reset_password')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return { id: r.userID }
})
}
const validateReset2FALink = token => {
if (!token) throw new authErrors.InvalidUrlError()
return users.validateAuthToken(token, 'reset_twofa')
.then(r => {
if (!r.success) throw new authErrors.InvalidUrlError()
return users.getUserById(r.userID)
})
.then(user => {
const secret = otplib.authenticator.generateSecret()
const otpauth = otplib.authenticator.keyuri(user.username, constants.AUTHENTICATOR_ISSUER_ENTITY, secret)
return Promise.all([users.saveTemp2FASecret(user.id, secret), user, secret, otpauth])
})
.then(([_, user, secret, otpauth]) => {
return { user_id: user.id, secret, otpauth }
})
}
const deleteSession = (sessionID, context) => {
destroySessionIfBeingUsed(sessionID, context)
return sessionManager.deleteSessionById(sessionID)
}
const login = (username, password) => {
return authenticateUser(username, password)
.then(user => {
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user.twofa_code])
})
.then(([devices, twoFASecret]) => {
if (!_.isEmpty(devices)) return 'FIDO'
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
})
}
const input2FA = (username, password, rememberMe, code, context) => {
return authenticateUser(username, password)
.then(user => {
const secret = user.twofa_code
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
initializeSession(context, user, rememberMe)
return true
})
}
const setup2FA = (username, password, rememberMe, codeConfirmation, context) => {
return authenticateUser(username, password)
.then(user => {
const isCodeValid = otplib.authenticator.verify({ token: codeConfirmation, secret: user.temp_twofa_code })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
initializeSession(context, user, rememberMe)
return users.save2FASecret(user.id, user.temp_twofa_code)
})
.then(() => true)
}
const changeUserRole = (code, id, newRole, context) => {
const action = () => users.changeUserRole(id, newRole)
return executeProtectedAction(code, id, context, action)
}
const enableUser = (code, id, context) => {
const action = () => users.enableUser(id)
return executeProtectedAction(code, id, context, action)
}
const disableUser = (code, id, context) => {
const action = () => users.disableUser(id)
return executeProtectedAction(code, id, context, action)
}
const createResetPasswordToken = (code, userID, context) => {
const action = () => authTokens.createAuthToken(userID, 'reset_password')
return executeProtectedAction(code, userID, context, action)
}
const createReset2FAToken = (code, userID, context) => {
const action = () => authTokens.createAuthToken(userID, 'reset_twofa')
return executeProtectedAction(code, userID, context, action)
}
const createRegisterToken = (username, role) => {
return users.getUserByUsername(username)
.then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.createUserRegistrationToken(username, role)
})
}
const register = (token, username, password, role) => {
return users.getUserByUsername(username)
.then(user => {
if (user) throw new authErrors.UserAlreadyExistsError()
return users.register(token, username, password, role).then(() => true)
})
}
const resetPassword = (token, userID, newPassword, context) => {
return users.getUserById(userID)
.then(user => {
destroySessionIfSameUser(context, user)
return users.updatePassword(token, user.id, newPassword)
})
.then(() => true)
}
const reset2FA = (token, userID, code, context) => {
return users.getUserById(userID)
.then(user => {
const isCodeValid = otplib.authenticator.verify({ token: code, secret: user.temp_twofa_code })
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
destroySessionIfSameUser(context, user)
return users.reset2FASecret(token, user.id, user.temp_twofa_code)
})
.then(() => true)
}
const getToken = context => {
if (_.isNil(context.req.cookies['lamassu_sid']) || _.isNil(context.req.session.user.id))
throw new authErrors.AuthenticationError('Authentication failed')
return context.req.session.user.id
}
module.exports = {
authenticateUser,
getUserData,
get2FASecret,
confirm2FA,
validateRegisterLink,
validateResetPasswordLink,
validateReset2FALink,
deleteSession,
login,
input2FA,
setup2FA,
changeUserRole,
enableUser,
disableUser,
createResetPasswordToken,
createReset2FAToken,
createRegisterToken,
register,
resetPassword,
reset2FA,
getToken
}

View file

@ -0,0 +1,9 @@
const bills = require('../../services/bills')
const resolvers = {
Query: {
bills: (...[, { filters }]) => bills.getBills(filters)
}
}
module.exports = resolvers

View file

@ -0,0 +1,18 @@
const blacklist = require('../../../blacklist')
const resolvers = {
Query: {
blacklist: () => blacklist.getBlacklist(),
blacklistMessages: () => blacklist.getMessages()
},
Mutation: {
deleteBlacklistRow: (...[, { address }]) =>
blacklist.deleteFromBlacklist(address),
insertBlacklistRow: (...[, { address }]) =>
blacklist.insertIntoBlacklist(address),
editBlacklistMessage: (...[, { id, content }]) =>
blacklist.editBlacklistMessage(id, content)
}
}
module.exports = resolvers

View file

@ -0,0 +1,17 @@
const { parseAsync } = require('json2csv')
const cashbox = require('../../../cashbox-batches')
const logDateFormat = require('../../../logs').logDateFormat
const resolvers = {
Query: {
cashboxBatches: () => cashbox.getBatches(),
cashboxBatchesCsv: (...[, { from, until, timezone }]) => cashbox.getBatches(from, until)
.then(data => parseAsync(logDateFormat(timezone, cashbox.logFormatter(data), ['created'])))
},
Mutation: {
createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount),
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
}
}
module.exports = resolvers

View file

@ -0,0 +1,11 @@
const { accounts: accountsConfig, countries, languages } = require('../../config')
const resolver = {
Query: {
countries: () => countries,
languages: () => languages,
accountsConfig: () => accountsConfig
}
}
module.exports = resolver

View file

@ -0,0 +1,10 @@
const { coins, currencies } = require('../../config')
const resolver = {
Query: {
currencies: () => currencies,
cryptoCurrencies: () => coins
}
}
module.exports = resolver

View file

@ -0,0 +1,33 @@
const authentication = require('../modules/userManagement')
const queries = require('../../services/customInfoRequests')
const DataLoader = require('dataloader')
const customerCustomInfoRequestsLoader = new DataLoader(ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), { cache: false })
const customInfoRequestLoader = new DataLoader(ids => queries.batchGetCustomInfoRequest(ids), { cache: false })
const resolvers = {
Customer: {
customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id)
},
CustomRequestData: {
customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId)
},
Query: {
customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled),
customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId),
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId)
},
Mutation: {
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => {
const token = authentication.getToken(context)
return queries.setAuthorizedCustomRequest(customerId, infoRequestId, override, token)
},
setCustomerCustomInfoRequest: (...[, { customerId, infoRequestId, data }]) => queries.setCustomerData(customerId, infoRequestId, data)
}
}
module.exports = resolvers

View file

@ -0,0 +1,66 @@
const authentication = require('../modules/userManagement')
const anonymous = require('../../../constants').anonymousCustomer
const customers = require('../../../customers')
const filters = require('../../filters')
const customerNotes = require('../../../customer-notes')
const machineLoader = require('../../../machine-loader')
const addLastUsedMachineName = customer =>
(customer.lastUsedMachine ? machineLoader.getMachineName(customer.lastUsedMachine) : Promise.resolve(null))
.then(lastUsedMachineName => Object.assign(customer, { lastUsedMachineName }))
const resolvers = {
Customer: {
isAnonymous: parent => (parent.customerId === anonymous.uuid)
},
Query: {
customers: (...[, { phone, email, name, address, id }]) => customers.getCustomersList(phone, name, address, id, email),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId).then(addLastUsedMachineName),
customerFilters: () => filters.customer()
},
Mutation: {
setCustomer: (root, { customerId, customerInput }, context, info) => {
const token = authentication.getToken(context)
if (customerId === anonymous.uuid) return customers.getCustomerById(customerId)
return customers.updateCustomer(customerId, customerInput, token)
},
addCustomField: (...[, { customerId, label, value }]) => customers.addCustomField(customerId, label, value),
saveCustomField: (...[, { customerId, fieldId, value }]) => customers.saveCustomField(customerId, fieldId, value),
removeCustomField: (...[, [ { customerId, fieldId } ]]) => customers.removeCustomField(customerId, fieldId),
editCustomer: async (root, { customerId, customerEdit }, context) => {
const token = authentication.getToken(context)
const editedData = await customerEdit
return customers.edit(customerId, editedData, token)
},
replacePhoto: async (root, { customerId, photoType, newPhoto }, context) => {
const token = authentication.getToken(context)
const { file } = newPhoto
const photo = await file
if (!photo) return customers.getCustomerById(customerId)
return customers.updateEditedPhoto(customerId, photo, photoType)
.then(newPatch => customers.edit(customerId, newPatch, token))
},
deleteEditedData: (root, { customerId, customerEdit }) => {
// TODO: NOT IMPLEMENTING THIS FEATURE FOR THE CURRENT VERSION
return customers.getCustomerById(customerId)
},
createCustomerNote: (...[, { customerId, title, content }, context]) => {
const token = authentication.getToken(context)
return customerNotes.createCustomerNote(customerId, token, title, content)
},
editCustomerNote: (...[, { noteId, newContent }, context]) => {
const token = authentication.getToken(context)
return customerNotes.updateCustomerNote(noteId, token, newContent)
},
deleteCustomerNote: (...[, { noteId }]) => {
return customerNotes.deleteCustomerNote(noteId)
},
createCustomer: (...[, { phoneNumber }]) => customers.add({ phone: phoneNumber }),
enableTestCustomer: (...[, { customerId }]) =>
customers.enableTestCustomer(customerId),
disableTestCustomer: (...[, { customerId }]) =>
customers.disableTestCustomer(customerId)
}
}
module.exports = resolvers

View file

@ -0,0 +1,9 @@
const funding = require('../../services/funding')
const resolvers = {
Query: {
funding: () => funding.getFunding()
}
}
module.exports = resolvers

View file

@ -0,0 +1,53 @@
const { mergeResolvers } = require('@graphql-tools/merge')
const bill = require('./bill.resolver')
const blacklist = require('./blacklist.resolver')
const cashbox = require('./cashbox.resolver')
const config = require('./config.resolver')
const currency = require('./currency.resolver')
const customer = require('./customer.resolver')
const customInfoRequests = require('./customInfoRequests.resolver')
const funding = require('./funding.resolver')
const log = require('./log.resolver')
const loyalty = require('./loyalty.resolver')
const machine = require('./machine.resolver')
const market = require('./market.resolver')
const notification = require('./notification.resolver')
const pairing = require('./pairing.resolver')
const rates = require('./rates.resolver')
const sanctions = require('./sanctions.resolver')
const scalar = require('./scalar.resolver')
const settings = require('./settings.resolver')
const sms = require('./sms.resolver')
const status = require('./status.resolver')
const transaction = require('./transaction.resolver')
const user = require('./users.resolver')
const version = require('./version.resolver')
const resolvers = [
bill,
blacklist,
cashbox,
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,
machine,
market,
notification,
pairing,
rates,
sanctions,
scalar,
settings,
sms,
status,
transaction,
user,
version
]
module.exports = mergeResolvers(resolvers)

View file

@ -0,0 +1,22 @@
const { parseAsync } = require('json2csv')
const _ = require('lodash/fp')
const logs = require('../../../logs')
const serverLogs = require('../../services/server-logs')
const resolvers = {
Query: {
machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
machineLogsCsv: (...[, { deviceId, from, until, limit, offset, timezone }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset)
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp']))),
serverLogs: (...[, { from, until, limit, offset }]) =>
serverLogs.getServerLogs(from, until, limit, offset),
serverLogsCsv: (...[, { from, until, limit, offset, timezone }]) =>
serverLogs.getServerLogs(from, until, limit, offset)
.then(res => parseAsync(logs.logDateFormat(timezone, res, ['timestamp'])))
}
}
module.exports = resolvers

View file

@ -0,0 +1,26 @@
const DataLoader = require('dataloader')
const loyalty = require('../../../loyalty')
const { getSlimCustomerByIdBatch } = require('../../../customers')
const customerLoader = new DataLoader(ids => {
return getSlimCustomerByIdBatch(ids)
}, { cache: false })
const resolvers = {
IndividualDiscount: {
customer: parent => customerLoader.load(parent.customerId)
},
Query: {
promoCodes: () => loyalty.getAvailablePromoCodes(),
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts()
},
Mutation: {
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
createIndividualDiscount: (...[, { customerId, discount }]) => loyalty.createIndividualDiscount(customerId, discount),
deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId)
}
}
module.exports = resolvers

View file

@ -0,0 +1,27 @@
const DataLoader = require('dataloader')
const { machineAction } = require('../../services/machines')
const machineLoader = require('../../../machine-loader')
const machineEventsByIdBatch = require('../../../postgresql_interface').machineEventsByIdBatch
const machineEventsLoader = new DataLoader(ids => {
return machineEventsByIdBatch(ids)
}, { cache: false })
const resolvers = {
Machine: {
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
},
Query: {
machines: () => machineLoader.getMachineNames(),
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
unpairedMachines: () => machineLoader.getUnpairedMachines()
},
Mutation: {
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
machineAction({ deviceId, action, cashUnits, newName }, context)
}
}
module.exports = resolvers

View file

@ -0,0 +1,9 @@
const exchange = require('../../../exchange')
const resolvers = {
Query: {
getMarkets: () => exchange.getMarkets()
}
}
module.exports = resolvers

View file

@ -0,0 +1,15 @@
const notifierQueries = require('../../../notifier/queries')
const resolvers = {
Query: {
notifications: () => notifierQueries.getNotifications(),
hasUnreadNotifications: () => notifierQueries.hasUnreadNotifications(),
alerts: () => notifierQueries.getAlerts()
},
Mutation: {
toggleClearNotification: (...[, { id, read }]) => notifierQueries.setRead(id, read),
clearAllNotifications: () => notifierQueries.markAllAsRead()
}
}
module.exports = resolvers

View file

@ -0,0 +1,9 @@
const pairing = require('../../services/pairing')
const resolvers = {
Mutation: {
createPairingTotem: (...[, { name }]) => pairing.totem(name)
}
}
module.exports = resolvers

View file

@ -0,0 +1,21 @@
const settingsLoader = require('../../../new-settings-loader')
const forex = require('../../../forex')
const plugins = require('../../../plugins')
const resolvers = {
Query: {
cryptoRates: () =>
settingsLoader.loadLatest().then(settings => {
const pi = plugins(settings)
return pi.getRawRates().then(r => {
return {
withCommissions: pi.buildRates(r),
withoutCommissions: pi.buildRatesNoCommission(r)
}
})
}),
fiatRates: () => forex.getFiatRates()
}
}
module.exports = resolvers

View file

@ -0,0 +1,13 @@
const sanctions = require('../../../sanctions')
const authentication = require('../modules/userManagement')
const resolvers = {
Query: {
checkAgainstSanctions: (...[, { customerId }, context]) => {
const token = authentication.getToken(context)
return sanctions.checkByUser(customerId, token)
}
}
}
module.exports = resolvers

View file

@ -0,0 +1,9 @@
const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars')
const resolvers = {
JSON: JSONResolver,
JSONObject: JSONObjectResolver,
DateTimeISO: DateTimeISOResolver
}
module.exports = resolvers

View file

@ -0,0 +1,14 @@
const settingsLoader = require('../../../new-settings-loader')
const resolvers = {
Query: {
accounts: () => settingsLoader.showAccounts(),
config: () => settingsLoader.loadLatestConfigOrNone()
},
Mutation: {
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config),
}
}
module.exports = resolvers

View file

@ -0,0 +1,14 @@
const smsNotices = require('../../../sms-notices')
const resolvers = {
Query: {
SMSNotices: () => smsNotices.getSMSNotices()
},
Mutation: {
editSMSNotice: (...[, { id, event, message }]) => smsNotices.editSMSNotice(id, event, message),
enableSMSNotice: (...[, { id }]) => smsNotices.enableSMSNotice(id),
disableSMSNotice: (...[, { id }]) => smsNotices.disableSMSNotice(id)
}
}
module.exports = resolvers

View file

@ -0,0 +1,9 @@
const supervisor = require('../../services/supervisor')
const resolvers = {
Query: {
uptime: () => supervisor.getAllProcessInfo()
}
}
module.exports = resolvers

View file

@ -0,0 +1,43 @@
const DataLoader = require('dataloader')
const { parseAsync } = require('json2csv')
const _ = require('lodash/fp')
const filters = require('../../filters')
const cashOutTx = require('../../../cash-out/cash-out-tx')
const cashInTx = require('../../../cash-in/cash-in-tx')
const transactions = require('../../services/transactions')
const anonymous = require('../../../constants').anonymousCustomer
const logDateFormat = require('../../../logs').logDateFormat
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids), { cache: false })
const resolvers = {
Customer: {
transactions: parent => transactionsLoader.load(parent.id)
},
Transaction: {
isAnonymous: parent => (parent.customerId === anonymous.uuid)
},
Query: {
transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
transactionCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTx(id, txClass).then(data =>
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt']))
),
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTxAssociatedData(id, txClass).then(data =>
parseAsync(logDateFormat(timezone, data, ['created']))
),
transactionFilters: () => filters.transaction()
},
Mutation: {
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id),
cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id)
}
}
module.exports = resolvers

View file

@ -0,0 +1,92 @@
const authentication = require('../modules/authentication')
const userManagement = require('../modules/userManagement')
const users = require('../../../users')
const sessionManager = require('../../../session-manager')
const getAttestationQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { userId: variables.userID, domain: variables.domain }
case 'FIDOPasswordless':
return { userId: variables.userID, domain: variables.domain }
case 'FIDOUsernameless':
return { userId: variables.userID, domain: variables.domain }
default:
return {}
}
}
const getAssertionQueryOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password, domain: variables.domain }
case 'FIDOPasswordless':
return { username: variables.username, domain: variables.domain }
case 'FIDOUsernameless':
return { domain: variables.domain }
default:
return {}
}
}
const getAttestationMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
case 'FIDOPasswordless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
case 'FIDOUsernameless':
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
default:
return {}
}
}
const getAssertionMutationOptions = variables => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
case 'FIDOPasswordless':
return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
case 'FIDOUsernameless':
return { assertionResponse: variables.assertionResponse, domain: variables.domain }
default:
return {}
}
}
const resolver = {
Query: {
users: () => users.getUsers(),
sessions: () => sessionManager.getSessions(),
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
userData: (...[, {}, context]) => userManagement.getUserData(context),
get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password),
confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context),
validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token),
validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token),
validateReset2FALink: (...[, { token }]) => userManagement.validateReset2FALink(token),
generateAttestationOptions: (...[, variables, context]) => authentication.strategy.generateAttestationOptions(context.req.session, getAttestationQueryOptions(variables)),
generateAssertionOptions: (...[, variables, context]) => authentication.strategy.generateAssertionOptions(context.req.session, getAssertionQueryOptions(variables))
},
Mutation: {
enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) => userManagement.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context),
reset2FA: (...[, { token, userID, code }, context]) => userManagement.reset2FA(token, userID, code, context),
validateAttestation: (...[, variables, context]) => authentication.strategy.validateAttestation(context.req.session, getAttestationMutationOptions(variables)),
validateAssertion: (...[, variables, context]) => authentication.strategy.validateAssertion(context.req.session, getAssertionMutationOptions(variables))
}
}
module.exports = resolver

View file

@ -0,0 +1,9 @@
const serverVersion = require('../../../../package.json').version
const resolvers = {
Query: {
serverVersion: () => serverVersion
}
}
module.exports = resolvers

View file

@ -0,0 +1,7 @@
const types = require('./types')
const resolvers = require('./resolvers')
module.exports = {
resolvers: resolvers,
typeDefs: types
}

View file

@ -0,0 +1,18 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Bill {
id: ID
fiat: Int
fiatCode: String
deviceId: ID
created: DateTimeISO
cashUnitOperationId: ID
}
type Query {
bills(filters: JSONObject): [Bill] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,28 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Blacklist {
address: String!
blacklistMessage: BlacklistMessage!
}
type BlacklistMessage {
id: ID
label: String
content: String
allowToggle: Boolean
}
type Query {
blacklist: [Blacklist] @auth
blacklistMessages: [BlacklistMessage] @auth
}
type Mutation {
deleteBlacklistRow(address: String!): Blacklist @auth
insertBlacklistRow(address: String!): Blacklist @auth
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,26 @@
const gql = require('graphql-tag')
const typeDef = gql`
type CashboxBatch {
id: ID
deviceId: ID
created: DateTimeISO
operationType: String
customBillCount: Int
performedBy: String
billCount: Int
fiatTotal: Int
}
type Query {
cashboxBatches: [CashboxBatch] @auth
cashboxBatchesCsv(from: DateTimeISO, until: DateTimeISO, timezone: String): String @auth
}
type Mutation {
createBatch(deviceId: ID, cashboxCount: Int): CashboxBatch @auth
editBatch(id: ID, performedBy: String): CashboxBatch @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,29 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Country {
code: String!
display: String!
}
type Language {
code: String!
display: String!
}
type AccountConfig {
code: String!
display: String!
class: String!
cryptos: [String]
deprecated: Boolean
}
type Query {
countries: [Country] @auth
languages: [Language] @auth
accountsConfig: [AccountConfig] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,22 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Currency {
code: String!
display: String!
}
type CryptoCurrency {
code: String!
display: String!
codeDisplay: String!
isBeta: Boolean
}
type Query {
currencies: [Currency] @auth
cryptoCurrencies: [CryptoCurrency] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,57 @@
const gql = require('graphql-tag')
const typeDef = gql`
type CustomInfoRequest {
id: ID!,
enabled: Boolean,
customRequest: JSON
}
input CustomRequestInputField {
choiceList: [String]
constraintType: String
type: String
numDigits: String
label1: String
label2: String
}
input CustomRequestInputScreen {
text: String
title: String
}
input CustomRequestInput {
name: String
input: CustomRequestInputField
screen1: CustomRequestInputScreen
screen2: CustomRequestInputScreen
}
type CustomRequestData {
customerId: ID
infoRequestId: ID
override: String
overrideAt: DateTimeISO
overrideBy: ID
customerData: JSON
customInfoRequest: CustomInfoRequest
}
type Query {
customInfoRequests(onlyEnabled: Boolean): [CustomInfoRequest] @auth
customerCustomInfoRequests(customerId: ID!): [CustomRequestData] @auth
customerCustomInfoRequest(customerId: ID!, infoRequestId: ID!): CustomRequestData @auth
}
type Mutation {
insertCustomInfoRequest(customRequest: CustomRequestInput!): CustomInfoRequest @auth
removeCustomInfoRequest(id: ID!): CustomInfoRequest @auth
editCustomInfoRequest(id: ID!, customRequest: CustomRequestInput!): CustomInfoRequest @auth
setAuthorizedCustomRequest(customerId: ID!, infoRequestId: ID!, override: String!): Boolean @auth
setCustomerCustomInfoRequest(customerId: ID!, infoRequestId: ID!, data: JSON!): Boolean @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,120 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Customer {
id: ID!
authorizedOverride: String
daysSuspended: Int
isSuspended: Boolean
newPhoto: Upload
photoType: String
frontCameraPath: String
frontCameraAt: DateTimeISO
frontCameraOverride: String
phone: String
email: String
isAnonymous: Boolean
smsOverride: String
idCardData: JSONObject
idCardDataOverride: String
idCardDataExpiration: DateTimeISO
idCardPhoto: Upload
idCardPhotoPath: String
idCardPhotoOverride: String
idCardPhotoAt: DateTimeISO
usSsn: String
usSsnOverride: String
sanctions: Boolean
sanctionsAt: DateTimeISO
sanctionsOverride: String
totalTxs: Int
totalSpent: String
lastActive: DateTimeISO
lastTxFiat: String
lastTxFiatCode: String
lastTxClass: String
lastUsedMachine: String
lastUsedMachineName: String
transactions: [Transaction]
subscriberInfo: JSONObject
phoneOverride: String
customFields: [CustomerCustomField]
customInfoRequests: [CustomRequestData]
notes: [CustomerNote]
isTestCustomer: Boolean
externalCompliance: [JSONObject]
}
input CustomerInput {
authorizedOverride: String
frontCameraPath: String
frontCameraOverride: String
phone: String
smsOverride: String
idCardData: JSONObject
idCardDataOverride: String
idCardDataExpiration: DateTimeISO
idCardPhotoPath: String
idCardPhotoOverride: String
usSsn: String
usSsnOverride: String
sanctions: Boolean
sanctionsAt: DateTimeISO
sanctionsOverride: String
totalTxs: Int
totalSpent: String
lastActive: DateTimeISO
lastTxFiat: String
lastTxFiatCode: String
lastTxClass: String
suspendedUntil: DateTimeISO
phoneOverride: String
}
input CustomerEdit {
idCardData: JSONObject
idCardPhoto: Upload
usSsn: String
subscriberInfo: JSONObject
}
type CustomerNote {
id: ID
customerId: ID
created: DateTimeISO
lastEditedAt: DateTimeISO
lastEditedBy: ID
title: String
content: String
}
type CustomerCustomField {
id: ID
label: String
value: String
}
type Query {
customers(phone: String, name: String, email: String, address: String, id: String): [Customer] @auth
customer(customerId: ID!): Customer @auth
customerFilters: [Filter] @auth
}
type Mutation {
setCustomer(customerId: ID!, customerInput: CustomerInput): Customer @auth
addCustomField(customerId: ID!, label: String!, value: String!): Boolean @auth
saveCustomField(customerId: ID!, fieldId: ID!, value: String!): Boolean @auth
removeCustomField(customerId: ID!, fieldId: ID!): Boolean @auth
editCustomer(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
deleteEditedData(customerId: ID!, customerEdit: CustomerEdit): Customer @auth
replacePhoto(customerId: ID!, photoType: String, newPhoto: Upload): Customer @auth
createCustomerNote(customerId: ID!, title: String!, content: String!): Boolean @auth
editCustomerNote(noteId: ID!, newContent: String!): Boolean @auth
deleteCustomerNote(noteId: ID!): Boolean @auth
createCustomer(phoneNumber: String): Customer @auth
enableTestCustomer(customerId: ID!): Boolean @auth
disableTestCustomer(customerId: ID!): Boolean @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,23 @@
const gql = require('graphql-tag')
const typeDef = gql`
type CoinFunds {
cryptoCode: String!
errorMsg: String
fundingAddress: String
fundingAddressUrl: String
confirmedBalance: String
pending: String
fiatConfirmedBalance: String
fiatPending: String
fiatCode: String
display: String
unitScale: String
}
type Query {
funding: [CoinFunds] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,53 @@
const { mergeTypeDefs } = require('@graphql-tools/merge')
const bill = require('./bill.type')
const blacklist = require('./blacklist.type')
const cashbox = require('./cashbox.type')
const config = require('./config.type')
const currency = require('./currency.type')
const customer = require('./customer.type')
const customInfoRequests = require('./customInfoRequests.type')
const funding = require('./funding.type')
const log = require('./log.type')
const loyalty = require('./loyalty.type')
const machine = require('./machine.type')
const market = require('./market.type')
const notification = require('./notification.type')
const pairing = require('./pairing.type')
const rates = require('./rates.type')
const sanctions = require('./sanctions.type')
const scalar = require('./scalar.type')
const settings = require('./settings.type')
const sms = require('./sms.type')
const status = require('./status.type')
const transaction = require('./transaction.type')
const user = require('./users.type')
const version = require('./version.type')
const types = [
bill,
blacklist,
cashbox,
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,
machine,
market,
notification,
pairing,
rates,
sanctions,
scalar,
settings,
sms,
status,
transaction,
user,
version
]
module.exports = mergeTypeDefs(types)

View file

@ -0,0 +1,26 @@
const gql = require('graphql-tag')
const typeDef = gql`
type MachineLog {
id: ID!
logLevel: String!
timestamp: DateTimeISO!
message: String!
}
type ServerLog {
id: ID!
logLevel: String!
timestamp: DateTimeISO!
message: String
}
type Query {
machineLogs(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [MachineLog] @auth
machineLogsCsv(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
serverLogs(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [ServerLog] @auth
serverLogsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,35 @@
const gql = require('graphql-tag')
const typeDef = gql`
type IndividualDiscount {
id: ID!
customer: DiscountCustomer!
discount: Int
}
type DiscountCustomer {
id: ID!
phone: String
idCardData: JSONObject
}
type PromoCode {
id: ID!
code: String!
discount: Int
}
type Query {
promoCodes: [PromoCode] @auth
individualDiscounts: [IndividualDiscount] @auth
}
type Mutation {
createPromoCode(code: String!, discount: Int!): PromoCode @auth
deletePromoCode(codeId: ID!): PromoCode @auth
createIndividualDiscount(customerId: ID!, discount: Int!): IndividualDiscount @auth
deleteIndividualDiscount(discountId: ID!): IndividualDiscount @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,105 @@
const gql = require('graphql-tag')
const typeDef = gql`
type MachineStatus {
label: String!
type: String!
}
type Machine {
name: String!
deviceId: ID!
paired: Boolean!
lastPing: DateTimeISO
pairedAt: DateTimeISO
diagnostics: Diagnostics
version: String
model: String
cashUnits: CashUnits
numberOfCassettes: Int
numberOfRecyclers: Int
statuses: [MachineStatus]
latestEvent: MachineEvent
downloadSpeed: String
responseTime: String
packetLoss: String
}
type Diagnostics {
timestamp: DateTimeISO
frontTimestamp: DateTimeISO
scanTimestamp: DateTimeISO
}
type CashUnits {
cashbox: Int
cassette1: Int
cassette2: Int
cassette3: Int
cassette4: Int
recycler1: Int
recycler2: Int
recycler3: Int
recycler4: Int
recycler5: Int
recycler6: Int
}
input CashUnitsInput {
cashbox: Int
cassette1: Int
cassette2: Int
cassette3: Int
cassette4: Int
recycler1: Int
recycler2: Int
recycler3: Int
recycler4: Int
recycler5: Int
recycler6: Int
}
type UnpairedMachine {
id: ID!
deviceId: ID!
name: String
model: String
paired: DateTimeISO!
unpaired: DateTimeISO!
}
type MachineEvent {
id: ID
deviceId: String
eventType: String
note: String
created: DateTimeISO
age: Float
deviceTime: DateTimeISO
}
enum MachineAction {
rename
resetCashOutBills
setCassetteBills
unpair
reboot
shutdown
restartServices
emptyUnit
refillUnit
diagnostics
}
type Query {
machines: [Machine] @auth
machine(deviceId: ID!): Machine @auth
unpairedMachines: [UnpairedMachine!]! @auth
}
type Mutation {
machineAction(deviceId:ID!, action: MachineAction!, cashUnits: CashUnitsInput, newName: String): Machine @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,9 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Query {
getMarkets: JSONObject @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,26 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Notification {
id: ID!
type: String
detail: JSON
message: String
created: DateTimeISO
read: Boolean
valid: Boolean
}
type Query {
notifications: [Notification] @auth
alerts: [Notification] @auth
hasUnreadNotifications: Boolean @auth
}
type Mutation {
toggleClearNotification(id: ID!, read: Boolean!): Notification @auth
clearAllNotifications: Notification @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,9 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Mutation {
createPairingTotem(name: String!): String @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,16 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Rate {
code: String
name: String
rate: Float
}
type Query {
cryptoRates: JSONObject @auth
fiatRates: [Rate] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,13 @@
const gql = require('graphql-tag')
const typeDef = gql`
type SanctionMatches {
ofacSanctioned: Boolean
}
type Query {
checkAgainstSanctions(customerId: ID): SanctionMatches @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,10 @@
const gql = require('graphql-tag')
const typeDef = gql`
scalar JSON
scalar JSONObject
scalar DateTimeISO
scalar Upload
`
module.exports = typeDef

View file

@ -0,0 +1,15 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Query {
accounts: JSONObject @auth
config: JSONObject @auth
}
type Mutation {
saveAccounts(accounts: JSONObject): JSONObject @auth
saveConfig(config: JSONObject): JSONObject @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,30 @@
const gql = require('graphql-tag')
const typeDef = gql`
type SMSNotice {
id: ID!
event: SMSNoticeEvent!
message: String!
messageName: String!
enabled: Boolean!
allowToggle: Boolean!
}
enum SMSNoticeEvent {
smsCode
cashOutDispenseReady
smsReceipt
}
type Query {
SMSNotices: [SMSNotice] @auth
}
type Mutation {
editSMSNotice(id: ID!, event: SMSNoticeEvent!, message: String!): SMSNotice @auth
enableSMSNotice(id: ID!): SMSNotice @auth
disableSMSNotice(id: ID!): SMSNotice @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,15 @@
const gql = require('graphql-tag')
const typeDef = gql`
type ProcessStatus {
name: String!
state: String!
uptime: Int!
}
type Query {
uptime: [ProcessStatus] @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,76 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Transaction {
id: ID!
txClass: String!
deviceId: ID!
toAddress: String
cryptoAtoms: String!
cryptoCode: String!
fiat: String!
fiatCode: String!
fee: String
txHash: String
phone: String
error: String
created: DateTimeISO
send: Boolean
sendConfirmed: Boolean
dispense: Boolean
timedout: Boolean
sendTime: DateTimeISO
errorCode: String
operatorCompleted: Boolean
sendPending: Boolean
fixedFee: String
minimumTx: Float
customerId: ID
isAnonymous: Boolean
txVersion: Int!
termsAccepted: Boolean
commissionPercentage: String
rawTickerPrice: String
isPaperWallet: Boolean
customerPhone: String
customerEmail: String
customerIdCardDataNumber: String
customerIdCardDataExpiration: DateTimeISO
customerIdCardData: JSONObject
customerName: String
customerFrontCameraPath: String
customerIdCardPhotoPath: String
expired: Boolean
machineName: String
discount: Int
txCustomerPhotoPath: String
txCustomerPhotoAt: DateTimeISO
batched: Boolean
batchTime: DateTimeISO
batchError: String
walletScore: Int
profit: String
swept: Boolean
}
type Filter {
type: String
value: String
label: String
}
type Query {
transactions(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth
transactionsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, txClass: String, deviceId: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
transactionFilters: [Filter] @auth
}
type Mutation {
cancelCashOutTransaction(id: ID): Transaction @auth
cancelCashInTransaction(id: ID): Transaction @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,114 @@
const authentication = require('../modules/authentication')
const getFIDOStrategyQueryTypes = () => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions(username: String!, password: String!, domain: String!): JSONObject`
case 'FIDOPasswordless':
return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions(username: String!, domain: String!): JSONObject`
case 'FIDOUsernameless':
return `generateAttestationOptions(userID: ID!, domain: String!): JSONObject
generateAssertionOptions(domain: String!): JSONObject`
default:
return ``
}
}
const getFIDOStrategyMutationsTypes = () => {
switch (authentication.CHOSEN_STRATEGY) {
case 'FIDO2FA':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(username: String!, password: String!, rememberMe: Boolean!, assertionResponse: JSONObject!, domain: String!): Boolean`
case 'FIDOPasswordless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(username: String!, rememberMe: Boolean!, assertionResponse: JSONObject!, domain: String!): Boolean`
case 'FIDOUsernameless':
return `validateAttestation(userID: ID!, attestationResponse: JSONObject!, domain: String!): Boolean
validateAssertion(assertionResponse: JSONObject!, domain: String!): Boolean`
default:
return ``
}
}
const typeDef = `
directive @auth(
requires: [Role] = [USER, SUPERUSER]
) on OBJECT | FIELD_DEFINITION
enum Role {
SUPERUSER
USER
}
type UserSession {
sid: String!
sess: JSONObject!
expire: DateTimeISO!
}
type User {
id: ID
username: String
role: String
enabled: Boolean
created: DateTimeISO
last_accessed: DateTimeISO
last_accessed_from: String
last_accessed_address: String
}
type TwoFactorSecret {
user_id: ID
secret: String!
otpauth: String!
}
type ResetToken {
token: String
user_id: ID
expire: DateTimeISO
}
type RegistrationToken {
token: String
username: String
role: String
expire: DateTimeISO
}
type Query {
users: [User] @auth(requires: [SUPERUSER])
sessions: [UserSession] @auth(requires: [SUPERUSER])
userSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
userData: User
get2FASecret(username: String!, password: String!): TwoFactorSecret
confirm2FA(code: String!): Boolean @auth(requires: [SUPERUSER])
validateRegisterLink(token: String!): User
validateResetPasswordLink(token: String!): User
validateReset2FALink(token: String!): TwoFactorSecret
${getFIDOStrategyQueryTypes()}
}
type Mutation {
enableUser(confirmationCode: String, id: ID!): User @auth(requires: [SUPERUSER])
disableUser(confirmationCode: String, id: ID!): User @auth(requires: [SUPERUSER])
deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER])
deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
changeUserRole(confirmationCode: String, id: ID!, newRole: String!): User @auth(requires: [SUPERUSER])
toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER])
login(username: String!, password: String!): String
input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean
setup2FA(username: String!, password: String!, rememberMe: Boolean!, codeConfirmation: String!): Boolean
createResetPasswordToken(confirmationCode: String, userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(confirmationCode: String, userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])
register(token: String!, username: String!, password: String!, role: String!): Boolean
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean
reset2FA(token: String!, userID: ID!, code: String!): Boolean
${getFIDOStrategyMutationsTypes()}
}
`
module.exports = typeDef

View file

@ -0,0 +1,9 @@
const gql = require('graphql-tag')
const typeDef = gql`
type Query {
serverVersion: String! @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,21 @@
const db = require('../../db')
const { USER_SESSIONS_TABLE_NAME } = require('../../constants')
const logger = require('../../logger')
let schemaCache = Date.now()
const cleanUserSessions = (cleanInterval) => (req, res, next) => {
const now = Date.now()
if (schemaCache + cleanInterval > now) return next()
logger.debug(`Clearing expired sessions for schema 'public'`)
return db.none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [USER_SESSIONS_TABLE_NAME, now])
.then(() => {
schemaCache = now
return next()
})
.catch(next)
}
module.exports = cleanUserSessions

View file

@ -0,0 +1,28 @@
const users = require('../../users')
const { AuthenticationError } = require('../graphql/errors')
const buildApolloContext = async ({ req, res }) => {
if (!req.session.user) return { req, res }
const user = await users.verifyAndUpdateUser(
req.session.user.id,
req.headers['user-agent'] || 'Unknown',
req.ip
)
if (!user || !user.enabled) throw new AuthenticationError('Authentication failed')
req.session.ua = req.headers['user-agent'] || 'Unknown'
req.session.ipAddress = req.ip
req.session.lastUsed = new Date(Date.now()).toISOString()
req.session.user.id = user.id
req.session.user.username = user.username
req.session.user.role = user.role
res.set('lamassu_role', user.role)
res.set('Access-Control-Expose-Headers', 'lamassu_role')
return { req, res }
}
module.exports = buildApolloContext

View file

@ -0,0 +1,9 @@
const cleanUserSessions = require('./cleanUserSessions')
const buildApolloContext = require('./context')
const session = require('./session')
module.exports = {
cleanUserSessions,
buildApolloContext,
session
}

View file

@ -0,0 +1,26 @@
const express = require('express')
const router = express.Router()
const session = require('express-session')
const PgSession = require('connect-pg-simple')(session)
const db = require('../../db')
const { USER_SESSIONS_TABLE_NAME } = require('../../constants')
const { getOperatorId } = require('../../operator')
router.use('*', async (req, res, next) => getOperatorId('authentication').then(operatorId => session({
store: new PgSession({
pgPromise: db,
tableName: USER_SESSIONS_TABLE_NAME
}),
name: 'lamassu_sid',
secret: operatorId,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: true
}
})(req, res, next))
)
module.exports = router

View file

@ -0,0 +1,31 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('../../db')
const getBills = filters => {
const deviceStatement = !_.isNil(filters.deviceId) ? `WHERE device_id = ${pgp.as.text(filters.deviceId)}` : ``
const batchStatement = filter => {
switch (filter) {
case 'none':
return `WHERE b.cashbox_batch_id IS NULL`
case 'any':
return `WHERE b.cashbox_batch_id IS NOT NULL`
default:
return _.isNil(filter) ? `` : `WHERE b.cashbox_batch_id = ${pgp.as.text(filter)}`
}
}
const sql = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (
SELECT id, device_id FROM cash_in_txs ${deviceStatement}
) AS cit ON cit.id = b.cash_in_txs_id ${batchStatement(filters.batch)} ${_.isNil(batchStatement(filters.batch)) ? `WHERE` : `AND`} b.destination_unit = 'cashbox'`
const sql2 = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, b.device_id FROM empty_unit_bills b ${deviceStatement} ${!_.isNil(filters.deviceId) && !_.isNil(filters.batch) ? `AND ${_.replace('WHERE', '', batchStatement(filters.batch))}` : `${batchStatement(filters.batch)}`}`
return Promise.all([db.any(sql), db.any(sql2)])
.then(([bills, operationalBills]) => _.map(_.mapKeys(_.camelCase), _.concat(bills, operationalBills)))
}
module.exports = {
getBills
}

View file

@ -0,0 +1,142 @@
const db = require('../../db')
const uuid = require('uuid')
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const { loadLatestConfigOrNone, saveConfig } = require('../../../lib/new-settings-loader')
const getCustomInfoRequests = (onlyEnabled = false) => {
const sql = onlyEnabled
? `SELECT * FROM custom_info_requests WHERE enabled = true ORDER BY custom_request->>'name'`
: `SELECT * FROM custom_info_requests ORDER BY custom_request->>'name'`
return db.any(sql).then(res => {
return res.map(item => ({
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}))
})
}
const addCustomInfoRequest = (customRequest) => {
const sql = 'INSERT INTO custom_info_requests (id, custom_request) VALUES ($1, $2)'
const id = uuid.v4()
return db.none(sql, [id, customRequest]).then(() => ({ id }))
}
const removeCustomInfoRequest = (id) => {
return loadLatestConfigOrNone()
.then(cfg => saveConfig({triggers: _.remove(x => x.customInfoRequestId === id, cfg.triggers ?? [])}))
.then(() => db.none('UPDATE custom_info_requests SET enabled = false WHERE id = $1', [id]))
.then(() => ({ id }));
}
const editCustomInfoRequest = (id, customRequest) => {
return db.none('UPDATE custom_info_requests SET custom_request = $1 WHERE id=$2', [customRequest, id]).then(() => ({ id, customRequest }))
}
const getAllCustomInfoRequestsForCustomer = (customerId) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1`
return db.any(sql, [customerId]).then(res => res.map(item => ({
customerId: item.customer_id,
infoRequestId: item.info_request_id,
customerData: item.customer_data,
override: item.override,
overrideAt: item.override_at,
overrideBy: item.override_by
})))
}
const getCustomInfoRequestForCustomer = (customerId, infoRequestId) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id = $1 AND info_request_id = $2`
return db.one(sql, [customerId, infoRequestId]).then(item => {
return {
customerId: item.customer_id,
infoRequestId: item.info_request_id,
customerData: item.customer_data,
override: item.override,
overrideAt: item.override_at,
overrideBy: item.override_by
}
})
}
const batchGetAllCustomInfoRequestsForCustomer = (customerIds) => {
const sql = `SELECT * FROM customers_custom_info_requests WHERE customer_id IN ($1^)`
return db.any(sql, [_.map(pgp.as.text, customerIds).join(',')]).then(res => {
const map = _.groupBy('customer_id', res)
return customerIds.map(id => {
const items = map[id] || []
return items.map(item => ({
customerId: item.customer_id,
infoRequestId: item.info_request_id,
customerData: item.customer_data,
override: item.override,
overrideAt: item.override_at,
overrideBy: item.override_by
}))
})
})
}
const getCustomInfoRequest = (infoRequestId) => {
const sql = `SELECT * FROM custom_info_requests WHERE id = $1`
return db.one(sql, [infoRequestId]).then(item => ({
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}))
}
const batchGetCustomInfoRequest = (infoRequestIds) => {
if (infoRequestIds.length === 0) return Promise.resolve([])
const sql = `SELECT * FROM custom_info_requests WHERE id IN ($1^)`
return db.any(sql, [_.map(pgp.as.text, infoRequestIds).join(',')]).then(res => {
const map = _.groupBy('id', res)
return infoRequestIds.map(id => {
const item = map[id][0] // since id is primary key the array always has 1 element
return {
id: item.id,
enabled: item.enabled,
customRequest: item.custom_request
}
})
})
}
const setAuthorizedCustomRequest = (customerId, infoRequestId, override, token) => {
const sql = `UPDATE customers_custom_info_requests SET override = $1, override_by = $2, override_at = now() WHERE customer_id = $3 AND info_request_id = $4`
return db.none(sql, [override, token, customerId, infoRequestId]).then(() => true)
}
const setCustomerData = (customerId, infoRequestId, data) => {
const sql = `
INSERT INTO customers_custom_info_requests (customer_id, info_request_id, customer_data)
VALUES ($1, $2, $3)
ON CONFLICT (customer_id, info_request_id)
DO UPDATE SET customer_data = $3`
return db.none(sql, [customerId, infoRequestId, data])
}
const setCustomerDataViaMachine = (customerId, infoRequestId, data) => {
const sql = `
INSERT INTO customers_custom_info_requests (customer_id, info_request_id, customer_data)
VALUES ($1, $2, $3)
ON CONFLICT (customer_id, info_request_id)
DO UPDATE SET customer_data = $3, override = $4, override_by = $5, override_at = now()`
return db.none(sql, [customerId, infoRequestId, data, 'automatic', null])
}
module.exports = {
getCustomInfoRequests,
addCustomInfoRequest,
removeCustomInfoRequest,
editCustomInfoRequest,
getAllCustomInfoRequestsForCustomer,
getCustomInfoRequestForCustomer,
batchGetAllCustomInfoRequestsForCustomer,
getCustomInfoRequest,
batchGetCustomInfoRequest,
setAuthorizedCustomRequest,
setCustomerData,
setCustomerDataViaMachine
}

View file

@ -0,0 +1,75 @@
const _ = require('lodash/fp')
const BN = require('../../bn')
const settingsLoader = require('../../new-settings-loader')
const configManager = require('../../new-config-manager')
const wallet = require('../../wallet')
const ticker = require('../../ticker')
const txBatching = require('../../tx-batching')
const { utils: coinUtils } = require('@lamassu/coins')
function computeCrypto (cryptoCode, _balance) {
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
const unitScale = cryptoRec.unitScale
return new BN(_balance).shiftedBy(-unitScale).decimalPlaces(5)
}
function computeFiat (rate, cryptoCode, _balance) {
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
const unitScale = cryptoRec.unitScale
return new BN(_balance).shiftedBy(-unitScale).times(rate).decimalPlaces(5)
}
function getSingleCoinFunding (settings, fiatCode, cryptoCode) {
const promises = [
wallet.newFunding(settings, cryptoCode),
ticker.getRates(settings, fiatCode, cryptoCode),
txBatching.getOpenBatchCryptoValue(cryptoCode)
]
return Promise.all(promises)
.then(([fundingRec, ratesRec, batchRec]) => {
const rates = ratesRec.rates
const rate = (rates.ask.plus(rates.bid)).div(2)
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
const pending = fundingRec.fundingPendingBalance.minus(batchRec)
const fiatPending = computeFiat(rate, cryptoCode, pending)
const fundingAddress = fundingRec.fundingAddress
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)
return {
cryptoCode,
fundingAddress,
fundingAddressUrl,
confirmedBalance: computeCrypto(cryptoCode, fundingConfirmedBalance).toFormat(5),
pending: computeCrypto(cryptoCode, pending).toFormat(5),
fiatConfirmedBalance: fiatConfirmedBalance,
fiatPending: fiatPending,
fiatCode
}
})
}
// Promise.allSettled not running on current version of node
const reflect = p => p.then(value => ({ value, status: 'fulfilled' }), error => ({ error: error.toString(), status: 'rejected' }))
function getFunding () {
return settingsLoader.loadLatest().then(settings => {
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
const fiatCode = configManager.getGlobalLocale(settings.config).fiatCurrency
const pareCoins = c => _.includes(c.cryptoCode, cryptoCodes)
const cryptoCurrencies = coinUtils.cryptoCurrencies()
const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies)
const promises = cryptoDisplays.map(it => getSingleCoinFunding(settings, fiatCode, it.cryptoCode))
return Promise.all(promises.map(reflect))
.then((response) => {
const mapped = response.map(it => _.merge({ errorMsg: it.error }, it.value))
return _.toArray(_.merge(mapped, cryptoDisplays))
})
})
}
module.exports = { getFunding }

View file

@ -0,0 +1,16 @@
const db = require('../../db')
function validateUser (username, password) {
return db.tx(t => {
const q1 = t.one('SELECT * FROM users WHERE username=$1 AND password=$2', [username, password])
const q2 = t.none('UPDATE users SET last_accessed = now() WHERE username=$1', [username])
return t.batch([q1, q2])
.then(([user]) => user)
.catch(() => false)
})
}
module.exports = {
validateUser
}

View file

@ -0,0 +1,20 @@
const machineLoader = require('../../machine-loader')
const { UserInputError } = require('../graphql/errors')
function getMachine (machineId) {
return machineLoader.getMachines()
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
}
function machineAction ({ deviceId, action, cashUnits, newName }, context) {
const operatorId = context.res.locals.operatorId
return getMachine(deviceId)
.then(machine => {
if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId })
return machine
})
.then(machineLoader.setMachine({ deviceId, action, cashUnits, newName }, operatorId))
.then(getMachine(deviceId))
}
module.exports = { machineAction }

View file

@ -0,0 +1,34 @@
const fs = require('fs')
const pify = require('pify')
const readFile = pify(fs.readFile)
const crypto = require('crypto')
const baseX = require('base-x')
const { parse, NIL } = require('uuid')
const db = require('../../db')
const pairing = require('../../pairing')
const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
const bsAlpha = baseX(ALPHA_BASE)
const CA_PATH = process.env.CA_PATH
const HOSTNAME = process.env.HOSTNAME
const unpair = pairing.unpair
function totem (name) {
return readFile(CA_PATH)
.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(() => bsAlpha.encode(buf))
})
}
module.exports = { totem, unpair }

View file

@ -0,0 +1,17 @@
const _ = require('lodash/fp')
const uuid = require('uuid')
const db = require('../../db')
function getServerLogs (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
const sql = `select id, log_level, timestamp, message from server_logs
where timestamp >= $1 and timestamp <= $2
order by timestamp desc
limit $3
offset $4`
return db.any(sql, [ from, until, limit, offset ])
.then(_.map(_.mapKeys(_.camelCase)))
}
module.exports = { getServerLogs }

View file

@ -0,0 +1,62 @@
const xmlrpc = require('xmlrpc')
const logger = require('../../logger')
const { promisify } = require('util')
// TODO new-admin: add the following to supervisor config
// [inet_http_server]
// port = 127.0.0.1:9001
function getAllProcessInfo () {
const convertStates = (state) => {
// From http://supervisord.org/subprocess.html#process-states
switch (state) {
case 'STOPPED':
return 'STOPPED'
case 'STARTING':
return 'RUNNING'
case 'RUNNING':
return 'RUNNING'
case 'BACKOFF':
return 'FATAL'
case 'STOPPING':
return 'STOPPED'
case 'EXITED':
return 'STOPPED'
case 'UNKNOWN':
return 'FATAL'
default:
logger.error(`Supervisord returned an unsupported state: ${state}`)
return 'FATAL'
}
}
const client = xmlrpc.createClient({
host: 'localhost',
port: '9001',
path: '/RPC2'
})
client.methodCall[promisify.custom] = (method, params) => {
return new Promise((resolve, reject) => client.methodCall(method, params, (err, value) => {
if (err) reject(err)
else resolve(value)
}))
}
return promisify(client.methodCall)('supervisor.getAllProcessInfo', [])
.then((value) => {
return value.map(process => (
{
name: process.name,
state: convertStates(process.statename),
uptime: (process.statename === 'RUNNING') ? process.now - process.start : 0
}
))
})
.catch((error) => {
if (error.code === 'ECONNREFUSED') logger.error('Failed to connect to supervisord HTTP server.')
else logger.error(error)
})
}
module.exports = { getAllProcessInfo }

View file

@ -0,0 +1,403 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('../../db')
const BN = require('../../bn')
const { utils: coinUtils } = require('@lamassu/coins')
const machineLoader = require('../../machine-loader')
const tx = require('../../tx')
const cashInTx = require('../../cash-in/cash-in-tx')
const { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/cash-out-helper')
const NUM_RESULTS = 1000
function addProfits (txs) {
return _.map(it => {
const profit = getProfit(it).toString()
return _.set('profit', profit, it)
}, txs)
}
const camelize = _.mapKeys(_.camelCase)
const DEVICE_NAME_QUERY = `
CASE
WHEN ud.name IS NOT NULL THEN ud.name || ' (unpaired)'
WHEN d.name IS NOT NULL THEN d.name
ELSE 'Unpaired'
END AS machine_name
`
const DEVICE_NAME_JOINS = `
LEFT JOIN devices d ON txs.device_id = d.device_id
LEFT JOIN (
SELECT device_id, name, unpaired, paired
FROM unpaired_devices
) ud ON txs.device_id = ud.device_id
AND ud.unpaired >= txs.created
AND (txs.created >= ud.paired)
`
function batch (
from = new Date(0).toISOString(),
until = new Date().toISOString(),
limit = null,
offset = 0,
txClass = null,
deviceId = null,
customerName = null,
fiatCode = null,
cryptoCode = null,
toAddress = null,
status = null,
swept = null,
excludeTestingCustomers = false,
simplified
) {
const isCsvExport = _.isBoolean(simplified)
const packager = _.flow(
_.flatten,
_.orderBy(_.property('created'), ['desc']),
_.map(_.flow(
camelize,
_.mapKeys(k =>
k == 'cashInFee' ? 'fixedFee' :
k
)
)),
addProfits
)
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
tb.error_message AS batch_error,
${DEVICE_NAME_QUERY}
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE txs.created >= $2 AND txs.created <= $3
AND ($6 is null or $6 = 'Cash In')
AND ($7 is null or txs.device_id = $7)
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
AND ($9 is null or txs.fiat_code = $9)
AND ($10 is null or txs.crypto_code = $10)
AND ($11 is null or txs.to_address = $11)
AND ($12 is null or txs.txStatus = $12)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'}
ORDER BY created DESC limit $4 offset $5`
const cashOutSql = `SELECT 'cashOut' AS tx_class,
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
array_to_string(array[c.id_card_data::json->>'firstName', c.id_card_data::json->>'lastName'], ' ') AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path,
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $1) AS expired,
${DEVICE_NAME_QUERY}
FROM (SELECT *, ${CASH_OUT_TRANSACTION_STATES} AS txStatus FROM cash_out_txs) txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress'
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
WHERE txs.created >= $2 AND txs.created <= $3
AND ($6 is null or $6 = 'Cash Out')
AND ($7 is null or txs.device_id = $7)
AND ($8 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $8)
AND ($9 is null or txs.fiat_code = $9)
AND ($10 is null or txs.crypto_code = $10)
AND ($11 is null or txs.to_address = $11)
AND ($12 is null or txs.txStatus = $12)
AND ($13 is null or txs.swept = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
${isCsvExport ? '' : 'AND fiat > 0'}
ORDER BY created DESC limit $4 offset $5`
// The swept filter is cash-out only, so omit the cash-in query entirely
const hasCashInOnlyFilters = false
const hasCashOutOnlyFilters = !_.isNil(swept)
let promises
if (hasCashInOnlyFilters && hasCashOutOnlyFilters) {
throw new Error('Trying to filter transactions with mutually exclusive filters')
}
if (hasCashInOnlyFilters) {
promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status])]
} else if (hasCashOutOnlyFilters) {
promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept])]
} else {
promises = [
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status]),
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept])
]
}
return Promise.all(promises)
.then(packager)
.then(res =>
!isCsvExport ? res :
// GQL transactions and transactionsCsv both use this function and
// if we don't check for the correct simplified value, the Transactions page polling
// will continuously build a csv in the background
simplified ? simplifiedBatch(res) :
advancedBatch(res)
)
}
function advancedBatch (data) {
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee',
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
'denomination1', 'denomination2', 'denomination3', 'denomination4',
'denominationRecycler1', 'denominationRecycler2', 'denominationRecycler3', 'denominationRecycler4', 'denominationRecycler5', 'denominationRecycler6',
'errorCode', 'customerId', 'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
'discount', 'txHash', 'customerPhone', 'customerEmail', 'customerIdCardDataNumber',
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
const addAdvancedFields = _.map(it => ({
...it,
status: getStatus(it),
fiatProfit: getProfit(it).toString(),
cryptoAmount: getCryptoAmount(it).toString(),
fixedFee: it.fixedFee ?? null,
fee: it.fee ?? null,
}))
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
}
function simplifiedBatch (data) {
const fields = ['txClass', 'id', 'created', 'machineName', 'fee',
'cryptoCode', 'cryptoAtoms', 'fiat', 'fiatCode', 'phone', 'email', 'toAddress',
'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount']
const addSimplifiedFields = _.map(it => ({
...it,
status: getStatus(it),
fiatProfit: getProfit(it).toString(),
cryptoAmount: getCryptoAmount(it).toString()
}))
return _.compose(_.map(_.pick(fields)), addSimplifiedFields)(data)
}
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode)
const getProfit = it => {
/* fiat - crypto*tickerPrice */
const calcCashInProfit = (fiat, crypto, tickerPrice) => fiat.minus(crypto.times(tickerPrice))
/* crypto*tickerPrice - fiat */
const calcCashOutProfit = (fiat, crypto, tickerPrice) => crypto.times(tickerPrice).minus(fiat)
const fiat = BN(it.fiat)
const crypto = getCryptoAmount(it)
const tickerPrice = BN(it.rawTickerPrice)
const isCashIn = it.txClass === 'cashIn'
return isCashIn
? calcCashInProfit(fiat, crypto, tickerPrice)
: calcCashOutProfit(fiat, crypto, tickerPrice)
}
const getCashOutStatus = it => {
if (it.hasError) return 'Error'
if (it.dispense) return 'Success'
if (it.expired) return 'Expired'
return 'Pending'
}
const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error'
if (it.batchError) return 'Error'
if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired'
return 'Pending'
}
const getStatus = it => {
if (it.txClass === 'cashOut') {
return getCashOutStatus(it)
}
return getCashInStatus(it)
}
function getCustomerTransactionsBatch (ids) {
const packager = _.flow(it => {
return it
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize))
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $2)) AS expired,
tb.error_message AS batch_error,
${DEVICE_NAME_QUERY}
FROM cash_in_txs AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE c.id IN ($1^)
ORDER BY created DESC limit $3`
const cashOutSql = `SELECT 'cashOut' AS tx_class,
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $3) AS expired,
${DEVICE_NAME_QUERY}
FROM cash_out_txs txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress'
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
WHERE c.id IN ($1^)
ORDER BY created DESC limit $2`
return Promise.all([
db.any(cashInSql, [_.map(pgp.as.text, ids).join(','), cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
db.any(cashOutSql, [_.map(pgp.as.text, ids).join(','), NUM_RESULTS, REDEEMABLE_AGE])
])
.then(packager).then(transactions => {
const transactionMap = _.groupBy('customerId', transactions)
return ids.map(id => transactionMap[id])
})
}
function single (txId) {
const packager = _.flow(_.compact, _.map(camelize))
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
tb.error_message AS batch_error,
${DEVICE_NAME_QUERY}
FROM cash_in_txs AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE id=$2`
const cashOutSql = `SELECT 'cashOut' AS tx_class,
txs.*,
actions.tx_hash,
c.phone AS customer_phone,
c.email AS customer_email,
c.id_card_data_number AS customer_id_card_data_number,
c.id_card_data_expiration AS customer_id_card_data_expiration,
c.id_card_data AS customer_id_card_data,
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path,
(NOT txs.dispense AND extract(epoch FROM (now() - greatest(txs.created, txs.confirmed_at))) >= $2) AS expired,
${DEVICE_NAME_QUERY}
FROM cash_out_txs txs
INNER JOIN cash_out_actions actions ON txs.id = actions.tx_id
AND actions.action = 'provisionAddress'
LEFT OUTER JOIN customers c ON txs.customer_id = c.id
${DEVICE_NAME_JOINS}
WHERE id=$1`
return Promise.all([
db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId]),
db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE])
])
.then(packager)
.then(_.head)
}
function cancel (txId) {
return tx.cancel(txId)
.then(() => single(txId))
}
function getTx (txId, txClass) {
const cashInSql = `select 'cashIn' as tx_class, txs.*,
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
from cash_in_txs as txs
where txs.id=$2`
const cashOutSql = `select 'cashOut' as tx_class,
txs.*,
(extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $2 as expired
from cash_out_txs txs
where txs.id=$1`
return txClass === 'cashIn'
? db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId])
: db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE])
}
function getTxAssociatedData (txId, txClass) {
const billsSql = `select 'bills' as bills, b.* from bills b where cash_in_txs_id = $1`
const actionsSql = `select 'cash_out_actions' as cash_out_actions, actions.* from cash_out_actions actions where tx_id = $1`
return txClass === 'cashIn'
? db.manyOrNone(billsSql, [txId])
: db.manyOrNone(actionsSql, [txId])
}
function updateTxCustomerPhoto (customerId, txId, direction, data) {
const formattedData = _.mapKeys(_.snakeCase, data)
const cashInSql = 'UPDATE cash_in_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
const cashOutSql = 'UPDATE cash_out_txs SET tx_customer_photo_at = $1, tx_customer_photo_path = $2 WHERE customer_id=$3 AND id=$4'
return direction === 'cashIn'
? db.oneOrNone(cashInSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId])
: db.oneOrNone(cashOutSql, [formattedData.tx_customer_photo_at, formattedData.tx_customer_photo_path, customerId, txId])
}
module.exports = {
batch,
single,
cancel,
getCustomerTransactionsBatch,
getTx,
getTxAssociatedData,
updateTxCustomerPhoto
}