v12.0.0 - initial commit

This commit is contained in:
padreug 2025-12-31 19:04:13 +01:00
commit e2c49ea43c
1145 changed files with 97211 additions and 0 deletions

View file

@ -0,0 +1,147 @@
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 { ResourceNotFoundError } = require('./graphql/errors')
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 || {}))
// Check by constructor name instead of instanceof due to ES module/CommonJS interop issues
if (error.originalError?.constructor?.name === 'NoResultError') {
return new ResourceNotFoundError()
}
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,213 @@
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 }

View file

@ -0,0 +1,250 @@
[
{ "code": "US", "display": "United States" },
{ "code": "GB", "display": "United Kingdom" },
{ "code": "CA", "display": "Canada" },
{ "code": "AU", "display": "Australia" },
{ "code": "AW", "display": "Aruba" },
{ "code": "AF", "display": "Afghanistan" },
{ "code": "AO", "display": "Angola" },
{ "code": "AI", "display": "Anguilla" },
{ "code": "AX", "display": "Åland Islands" },
{ "code": "AL", "display": "Albania" },
{ "code": "AD", "display": "Andorra" },
{ "code": "AE", "display": "United Arab Emirates" },
{ "code": "AR", "display": "Argentina" },
{ "code": "AM", "display": "Armenia" },
{ "code": "AS", "display": "American Samoa" },
{ "code": "AQ", "display": "Antarctica" },
{ "code": "TF", "display": "French Southern and Antarctic Lands" },
{ "code": "AG", "display": "Antigua and Barbuda" },
{ "code": "AT", "display": "Austria" },
{ "code": "AZ", "display": "Azerbaijan" },
{ "code": "BI", "display": "Burundi" },
{ "code": "BE", "display": "Belgium" },
{ "code": "BJ", "display": "Benin" },
{ "code": "BF", "display": "Burkina Faso" },
{ "code": "BD", "display": "Bangladesh" },
{ "code": "BG", "display": "Bulgaria" },
{ "code": "BH", "display": "Bahrain" },
{ "code": "BS", "display": "Bahamas" },
{ "code": "BA", "display": "Bosnia and Herzegovina" },
{ "code": "BL", "display": "Saint Barthélemy" },
{ "code": "BY", "display": "Belarus" },
{ "code": "BZ", "display": "Belize" },
{ "code": "BM", "display": "Bermuda" },
{ "code": "BO", "display": "Bolivia" },
{ "code": "BR", "display": "Brazil" },
{ "code": "BB", "display": "Barbados" },
{ "code": "BN", "display": "Brunei" },
{ "code": "BT", "display": "Bhutan" },
{ "code": "BV", "display": "Bouvet Island" },
{ "code": "BW", "display": "Botswana" },
{ "code": "CF", "display": "Central African Republic" },
{ "code": "CC", "display": "Cocos (Keeling) Islands" },
{ "code": "CH", "display": "Switzerland" },
{ "code": "CL", "display": "Chile" },
{ "code": "CN", "display": "China" },
{ "code": "CI", "display": "Ivory Coast" },
{ "code": "CM", "display": "Cameroon" },
{ "code": "CD", "display": "DR Congo" },
{ "code": "CG", "display": "Republic of the Congo" },
{ "code": "CK", "display": "Cook Islands" },
{ "code": "CO", "display": "Colombia" },
{ "code": "KM", "display": "Comoros" },
{ "code": "CV", "display": "Cape Verde" },
{ "code": "CR", "display": "Costa Rica" },
{ "code": "CU", "display": "Cuba" },
{ "code": "CW", "display": "Curaçao" },
{ "code": "CX", "display": "Christmas Island" },
{ "code": "KY", "display": "Cayman Islands" },
{ "code": "CY", "display": "Cyprus" },
{ "code": "CZ", "display": "Czech Republic" },
{ "code": "DE", "display": "Germany" },
{ "code": "DJ", "display": "Djibouti" },
{ "code": "DM", "display": "Dominica" },
{ "code": "DK", "display": "Denmark" },
{ "code": "DO", "display": "Dominican Republic" },
{ "code": "DZ", "display": "Algeria" },
{ "code": "EC", "display": "Ecuador" },
{ "code": "EG", "display": "Egypt" },
{ "code": "ER", "display": "Eritrea" },
{ "code": "EH", "display": "Western Sahara" },
{ "code": "ES", "display": "Spain" },
{ "code": "EE", "display": "Estonia" },
{ "code": "ET", "display": "Ethiopia" },
{ "code": "FI", "display": "Finland" },
{ "code": "FJ", "display": "Fiji" },
{ "code": "FK", "display": "Falkland Islands" },
{ "code": "FR", "display": "France" },
{ "code": "FO", "display": "Faroe Islands" },
{ "code": "FM", "display": "Micronesia" },
{ "code": "GA", "display": "Gabon" },
{ "code": "GE", "display": "Georgia" },
{ "code": "GG", "display": "Guernsey" },
{ "code": "GH", "display": "Ghana" },
{ "code": "GI", "display": "Gibraltar" },
{ "code": "GN", "display": "Guinea" },
{ "code": "GP", "display": "Guadeloupe" },
{ "code": "GM", "display": "Gambia" },
{ "code": "GW", "display": "Guinea-Bissau" },
{ "code": "GQ", "display": "Equatorial Guinea" },
{ "code": "GR", "display": "Greece" },
{ "code": "GD", "display": "Grenada" },
{ "code": "GL", "display": "Greenland" },
{ "code": "GT", "display": "Guatemala" },
{ "code": "GF", "display": "French Guiana" },
{ "code": "GU", "display": "Guam" },
{ "code": "GY", "display": "Guyana" },
{ "code": "HK", "display": "Hong Kong" },
{ "code": "HM", "display": "Heard Island and McDonald Islands" },
{ "code": "HN", "display": "Honduras" },
{ "code": "HR", "display": "Croatia" },
{ "code": "HT", "display": "Haiti" },
{ "code": "HU", "display": "Hungary" },
{ "code": "ID", "display": "Indonesia" },
{ "code": "IM", "display": "Isle of Man" },
{ "code": "IN", "display": "India" },
{ "code": "IO", "display": "British Indian Ocean Territory" },
{ "code": "IE", "display": "Ireland" },
{ "code": "IR", "display": "Iran" },
{ "code": "IQ", "display": "Iraq" },
{ "code": "IS", "display": "Iceland" },
{ "code": "IL", "display": "Israel" },
{ "code": "IT", "display": "Italy" },
{ "code": "JM", "display": "Jamaica" },
{ "code": "JE", "display": "Jersey" },
{ "code": "JO", "display": "Jordan" },
{ "code": "JP", "display": "Japan" },
{ "code": "KZ", "display": "Kazakhstan" },
{ "code": "KE", "display": "Kenya" },
{ "code": "KG", "display": "Kyrgyzstan" },
{ "code": "KH", "display": "Cambodia" },
{ "code": "KI", "display": "Kiribati" },
{ "code": "KN", "display": "Saint Kitts and Nevis" },
{ "code": "KR", "display": "South Korea" },
{ "code": "XK", "display": "Kosovo" },
{ "code": "KW", "display": "Kuwait" },
{ "code": "LA", "display": "Laos" },
{ "code": "LB", "display": "Lebanon" },
{ "code": "LR", "display": "Liberia" },
{ "code": "LY", "display": "Libya" },
{ "code": "LC", "display": "Saint Lucia" },
{ "code": "LI", "display": "Liechtenstein" },
{ "code": "LK", "display": "Sri Lanka" },
{ "code": "LS", "display": "Lesotho" },
{ "code": "LT", "display": "Lithuania" },
{ "code": "LU", "display": "Luxembourg" },
{ "code": "LV", "display": "Latvia" },
{ "code": "MO", "display": "Macau" },
{ "code": "MF", "display": "Saint Martin" },
{ "code": "MA", "display": "Morocco" },
{ "code": "MC", "display": "Monaco" },
{ "code": "MD", "display": "Moldova" },
{ "code": "MG", "display": "Madagascar" },
{ "code": "MV", "display": "Maldives" },
{ "code": "MX", "display": "Mexico" },
{ "code": "MH", "display": "Marshall Islands" },
{ "code": "MK", "display": "Macedonia" },
{ "code": "ML", "display": "Mali" },
{ "code": "MT", "display": "Malta" },
{ "code": "MM", "display": "Myanmar" },
{ "code": "ME", "display": "Montenegro" },
{ "code": "MN", "display": "Mongolia" },
{ "code": "MP", "display": "Northern Mariana Islands" },
{ "code": "MZ", "display": "Mozambique" },
{ "code": "MR", "display": "Mauritania" },
{ "code": "MS", "display": "Montserrat" },
{ "code": "MQ", "display": "Martinique" },
{ "code": "MU", "display": "Mauritius" },
{ "code": "MW", "display": "Malawi" },
{ "code": "MY", "display": "Malaysia" },
{ "code": "YT", "display": "Mayotte" },
{ "code": "NA", "display": "Namibia" },
{ "code": "NC", "display": "New Caledonia" },
{ "code": "NE", "display": "Niger" },
{ "code": "NF", "display": "Norfolk Island" },
{ "code": "NG", "display": "Nigeria" },
{ "code": "NI", "display": "Nicaragua" },
{ "code": "NU", "display": "Niue" },
{ "code": "NL", "display": "Netherlands" },
{ "code": "NO", "display": "Norway" },
{ "code": "NP", "display": "Nepal" },
{ "code": "NR", "display": "Nauru" },
{ "code": "NZ", "display": "New Zealand" },
{ "code": "OM", "display": "Oman" },
{ "code": "PK", "display": "Pakistan" },
{ "code": "PA", "display": "Panama" },
{ "code": "PN", "display": "Pitcairn Islands" },
{ "code": "PE", "display": "Peru" },
{ "code": "PH", "display": "Philippines" },
{ "code": "PW", "display": "Palau" },
{ "code": "PG", "display": "Papua New Guinea" },
{ "code": "PL", "display": "Poland" },
{ "code": "PR", "display": "Puerto Rico" },
{ "code": "KP", "display": "North Korea" },
{ "code": "PT", "display": "Portugal" },
{ "code": "PY", "display": "Paraguay" },
{ "code": "PS", "display": "Palestine" },
{ "code": "PF", "display": "French Polynesia" },
{ "code": "QA", "display": "Qatar" },
{ "code": "RE", "display": "Réunion" },
{ "code": "RO", "display": "Romania" },
{ "code": "RU", "display": "Russia" },
{ "code": "RW", "display": "Rwanda" },
{ "code": "SA", "display": "Saudi Arabia" },
{ "code": "SD", "display": "Sudan" },
{ "code": "SN", "display": "Senegal" },
{ "code": "SG", "display": "Singapore" },
{ "code": "GS", "display": "South Georgia" },
{ "code": "SJ", "display": "Svalbard and Jan Mayen" },
{ "code": "SB", "display": "Solomon Islands" },
{ "code": "SL", "display": "Sierra Leone" },
{ "code": "SV", "display": "El Salvador" },
{ "code": "SM", "display": "San Marino" },
{ "code": "SO", "display": "Somalia" },
{ "code": "PM", "display": "Saint Pierre and Miquelon" },
{ "code": "RS", "display": "Serbia" },
{ "code": "SS", "display": "South Sudan" },
{ "code": "ST", "display": "São Tomé and Príncipe" },
{ "code": "SR", "display": "Suriname" },
{ "code": "SK", "display": "Slovakia" },
{ "code": "SI", "display": "Slovenia" },
{ "code": "SE", "display": "Sweden" },
{ "code": "SZ", "display": "Swaziland" },
{ "code": "SX", "display": "Sint Maarten" },
{ "code": "SC", "display": "Seychelles" },
{ "code": "SY", "display": "Syria" },
{ "code": "TC", "display": "Turks and Caicos Islands" },
{ "code": "TD", "display": "Chad" },
{ "code": "TG", "display": "Togo" },
{ "code": "TH", "display": "Thailand" },
{ "code": "TJ", "display": "Tajikistan" },
{ "code": "TK", "display": "Tokelau" },
{ "code": "TM", "display": "Turkmenistan" },
{ "code": "TL", "display": "Timor-Leste" },
{ "code": "TO", "display": "Tonga" },
{ "code": "TT", "display": "Trinidad and Tobago" },
{ "code": "TN", "display": "Tunisia" },
{ "code": "TR", "display": "Turkey" },
{ "code": "TV", "display": "Tuvalu" },
{ "code": "TW", "display": "Taiwan" },
{ "code": "TZ", "display": "Tanzania" },
{ "code": "UG", "display": "Uganda" },
{ "code": "UA", "display": "Ukraine" },
{ "code": "UM", "display": "United States Minor Outlying Islands" },
{ "code": "UY", "display": "Uruguay" },
{ "code": "UZ", "display": "Uzbekistan" },
{ "code": "VA", "display": "Vatican City" },
{ "code": "VC", "display": "Saint Vincent and the Grenadines" },
{ "code": "VE", "display": "Venezuela" },
{ "code": "VG", "display": "British Virgin Islands" },
{ "code": "VI", "display": "United States Virgin Islands" },
{ "code": "VN", "display": "Vietnam" },
{ "code": "VU", "display": "Vanuatu" },
{ "code": "WF", "display": "Wallis and Futuna" },
{ "code": "WS", "display": "Samoa" },
{ "code": "YE", "display": "Yemen" },
{ "code": "ZA", "display": "South Africa" },
{ "code": "ZM", "display": "Zambia" },
{ "code": "ZW", "display": "Zimbabwe" }
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,266 @@
{
"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,31 @@
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)
}
module.exports = { transaction }

View file

@ -0,0 +1,53 @@
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,107 @@
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,
},
})
}
}
class ResourceNotFoundError extends GraphQLError {
constructor(details = {}) {
super('Resource not found', {
extensions: {
code: 'RESOURCE_NOT_FOUND',
...details,
},
})
}
}
class ResourceAlreadyExistsError extends GraphQLError {
constructor(details = {}) {
super('Resource already exists', {
extensions: {
code: 'RESOURCE_ALREADY_EXISTS',
...details,
},
})
}
}
class ResourceHasDependenciesError extends GraphQLError {
constructor(details = {}) {
super('Resource has dependencies', {
extensions: {
code: 'RESOURCE_HAS_DEPENDENCIES',
...details,
},
})
}
}
module.exports = {
AuthenticationError,
InvalidCredentialsError,
UserAlreadyExistsError,
InvalidTwoFactorError,
InvalidUrlError,
UserInputError,
ResourceNotFoundError,
ResourceAlreadyExistsError,
ResourceHasDependenciesError,
}

View file

@ -0,0 +1,199 @@
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,187 @@
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) {
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,193 @@
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,312 @@
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,25 @@
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,15 @@
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,56 @@
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,84 @@
const authentication = require('../modules/userManagement')
const anonymous = require('../../../constants').anonymousCustomer
const customers = require('../../../customers')
const customerNotes = require('../../../customer-notes')
const machineLoader = require('../../../machine-loader')
const {
customers: { searchCustomers },
} = require('typesafe-db')
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: () => customers.getCustomersList(),
customer: (...[, { customerId }]) =>
customers.getCustomerById(customerId).then(addLastUsedMachineName),
searchCustomers: (...[, { searchTerm, limit = 20 }]) =>
searchCustomers(searchTerm, limit),
},
Mutation: {
setCustomer: (root, { customerId, customerInput }, context) => {
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 }) => {
// 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,57 @@
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 machineGroups = require('./machineGroups.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 triggers = require('./triggers.resolver')
const resolvers = [
bill,
blacklist,
cashbox,
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,
machine,
machineGroups,
market,
notification,
pairing,
rates,
sanctions,
scalar,
settings,
sms,
status,
transaction,
user,
version,
triggers,
]
module.exports = mergeResolvers(resolvers)

View file

@ -0,0 +1,29 @@
const { parseAsync } = require('json2csv')
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,32 @@
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,33 @@
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: {
assignMachinesToGroup: (...[, { deviceIds, groupId }]) =>
machineLoader.assignToGroup(deviceIds, groupId),
machineAction: (...[, { deviceId, action, cashUnits, newName }, context]) =>
machineAction({ deviceId, action, cashUnits, newName }, context),
},
}
module.exports = resolvers

View file

@ -0,0 +1,39 @@
const DataLoader = require('dataloader')
const {
getAllMachineGroups,
createMachineGroup,
deleteMachineGroup,
assignComplianceTriggerSetToMachineGroup,
} = require('../../services/machineGroups')
const {
getComplianceTriggerSetsByIdsBatch,
} = require('../../services/triggers')
const complianceTriggerSetsLoader = new DataLoader(
ids => getComplianceTriggerSetsByIdsBatch(ids),
{ cache: false },
)
const resolvers = {
MachineGroup: {
complianceTriggerSet: parent =>
parent.complianceTriggerSetId
? complianceTriggerSetsLoader.load(parent.complianceTriggerSetId)
: null,
},
Query: {
machineGroups: () => getAllMachineGroups(),
},
Mutation: {
createMachineGroup: (...[, { name }]) => createMachineGroup(name),
deleteMachineGroup: (...[, { id }]) => deleteMachineGroup(id),
assignComplianceTriggerSetToMachineGroup: (
source,
{ id, complianceTriggerSetId },
) => assignComplianceTriggerSetToMachineGroup(id, complianceTriggerSetId),
},
}
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,16 @@
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.load().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,13 @@
const {
DateTimeISOResolver,
JSONResolver,
JSONObjectResolver,
} = require('graphql-scalars')
const resolvers = {
JSON: JSONResolver,
JSONObject: JSONObjectResolver,
DateTimeISO: DateTimeISOResolver,
}
module.exports = resolvers

View file

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

View file

@ -0,0 +1,15 @@
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,13 @@
const supervisor = require('../../services/supervisor')
const {
getCachedRestrictionLevel,
} = require('../../services/restriction-level')
const resolvers = {
Query: {
uptime: () => supervisor.getAllProcessInfo(),
restrictionLevel: () => getCachedRestrictionLevel(),
},
}
module.exports = resolvers

View file

@ -0,0 +1,126 @@
const { parseAsync } = require('json2csv')
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 resolvers = {
Transaction: {
isAnonymous: parent => parent.customerId === anonymous.uuid,
},
Query: {
transactions: (
...[
,
{
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
customerId,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
excludeTestingCustomers,
},
]
) =>
transactions.batch({
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
customerId,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
excludeTestingCustomers,
}),
transactionsCsv: (
...[
,
{
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
customerId,
fiatCode,
cryptoCode,
toAddress,
status,
swept,
timezone,
excludeTestingCustomers,
simplified,
},
]
) =>
transactions
.batch({
from,
until,
limit,
offset,
txClass,
deviceId,
customerName,
customerId,
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,48 @@
const {
getComplianceTriggerSets,
getComplianceTriggerSetById,
getComplianceTriggers,
createComplianceTriggerSet,
deleteComplianceTriggerSet,
createComplianceTrigger,
deleteComplianceTrigger,
} = require('../../services/triggers')
const Query = {
complianceTriggerSets() {
return getComplianceTriggerSets()
},
complianceTriggerSetById(source, { id }) {
return getComplianceTriggerSetById(id)
},
complianceTriggers(source, { complianceTriggerSetId }) {
return getComplianceTriggers(complianceTriggerSetId)
},
}
const Mutation = {
createComplianceTriggerSet(source, { name }) {
return createComplianceTriggerSet(name)
},
deleteComplianceTriggerSet(source, { id }) {
return deleteComplianceTriggerSet(id)
},
createComplianceTrigger(source, { complianceTriggerSetId, trigger }) {
return createComplianceTrigger(complianceTriggerSetId, trigger).then(
() => true,
)
},
deleteComplianceTrigger(source, { id }) {
return deleteComplianceTrigger(id).then(() => true)
},
}
module.exports = {
Query,
Mutation,
}

View file

@ -0,0 +1,170 @@
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,30 @@
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,73 @@
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
disablePermissionScreen: Boolean
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,146 @@
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 CustomerSearchResult {
id: ID!
name: String
phone: String
email: String
}
type Query {
customers(
phone: String
name: String
email: String
address: String
id: String
): [Customer] @auth
customer(customerId: ID!): Customer @auth
customerFilters: [Filter] @auth
searchCustomers(searchTerm: String!, limit: Int): [CustomerSearchResult]
@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,57 @@
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 machineGroups = require('./machineGroups.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 triggers = require('./triggers.type')
const types = [
bill,
blacklist,
cashbox,
config,
currency,
customer,
customInfoRequests,
funding,
log,
loyalty,
machine,
machineGroups,
market,
notification,
pairing,
rates,
sanctions,
scalar,
settings,
sms,
status,
transaction,
user,
version,
triggers,
]
module.exports = mergeTypeDefs(types)

View file

@ -0,0 +1,50 @@
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,38 @@
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,112 @@
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
machineGroup: MachineGroup
}
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 {
assignMachinesToGroup(deviceIds: [ID!]!, groupId: ID!): [ID]
machineAction(
deviceId: ID!
action: MachineAction!
cashUnits: CashUnitsInput
newName: String
): Machine @auth
}
`
module.exports = typeDef

View file

@ -0,0 +1,26 @@
const gql = require('graphql-tag')
const typeDef = gql`
type MachineGroup {
id: ID!
name: String!
complianceTriggerSetId: ID
complianceTriggerSet: ComplianceTriggerSet
deviceCount: Int
}
type Query {
machineGroups: [MachineGroup!]! @auth
}
type Mutation {
createMachineGroup(name: String!): MachineGroup! @auth
deleteMachineGroup(id: ID!): MachineGroup @auth
assignComplianceTriggerSetToMachineGroup(
id: ID!
complianceTriggerSetId: ID
): MachineGroup! @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,31 @@
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,16 @@
const gql = require('graphql-tag')
const typeDef = gql`
type ProcessStatus {
name: String!
state: String!
uptime: Int!
}
type Query {
uptime: [ProcessStatus] @auth
restrictionLevel: Int
}
`
module.exports = typeDef

View file

@ -0,0 +1,111 @@
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
isAnonymous: Boolean
txVersion: Int!
termsAccepted: Boolean
commissionPercentage: String
rawTickerPrice: String
isPaperWallet: Boolean
expired: Boolean
machineName: String
discount: Int
customerId: ID
customerPhone: String
customerEmail: String
customerIdCardData: JSONObject
customerFrontCameraPath: String
customerIdCardPhotoPath: String
txCustomerPhotoPath: String
txCustomerPhotoAt: DateTimeISO
batched: Boolean
batchTime: DateTimeISO
batchError: String
walletScore: Int
profit: String
swept: Boolean
status: String
paginationStats: PaginationStats
}
type PaginationStats {
totalCount: Int
}
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
customerId: ID
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
customerId: ID
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,73 @@
const gql = require('graphql-tag')
const typeDef = gql`
type ComplianceTriggerSet {
id: ID!
name: String!
}
enum TriggerType {
txAmount
txVolume
txVelocity
consecutiveDays
}
enum RequirementType {
sms
idCardPhoto
idCardData
facephoto
sanctions
usSsn
suspend
block
external
custom
}
type ComplianceTrigger {
id: ID!
direction: String!
triggerType: TriggerType!
requirementType: RequirementType!
suspensionDays: Float
threshold: Int
thresholdDays: Int
customInfoRequestId: ID
externalService: String
}
input ComplianceTriggerInput {
id: ID!
direction: String!
triggerType: TriggerType!
requirementType: RequirementType!
suspensionDays: Float
threshold: Int
thresholdDays: Int
customInfoRequestId: ID
externalService: String
}
type Query {
complianceTriggerSets: [ComplianceTriggerSet!]! @auth
complianceTriggerSetById(id: ID!): ComplianceTriggerSet! @auth
complianceTriggers(complianceTriggerSetId: ID!): [ComplianceTrigger!]! @auth
}
type Mutation {
createComplianceTriggerSet(name: String!): ComplianceTriggerSet @auth
deleteComplianceTriggerSet(id: ID!): ComplianceTriggerSet @auth
createComplianceTrigger(
complianceTriggerSetId: ID!
trigger: ComplianceTriggerInput!
): Boolean! @auth
deleteComplianceTrigger(id: ID!): Boolean! @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,25 @@
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,29 @@
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,29 @@
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,60 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('../../db')
const AND = (...clauses) => clauses.filter(clause => !!clause).join(' AND ')
const getBatchIDCondition = filter => {
switch (filter) {
case 'none':
return 'b.cashbox_batch_id IS NULL'
case 'any':
return 'b.cashbox_batch_id IS NOT NULL'
default:
return _.isNil(filter)
? ''
: `b.cashbox_batch_id = ${pgp.as.text(filter)}`
}
}
const getBills = filters => {
const deviceIDCondition = !_.isNil(filters.deviceId)
? `device_id = ${pgp.as.text(filters.deviceId)}`
: ''
const batchIDCondition = getBatchIDCondition(filters.batch)
const cashboxBills = `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
WHERE ${AND(
deviceIDCondition,
'device_id IN (SELECT device_id FROM devices WHERE paired)',
)}
) AS cit
ON cit.id = b.cash_in_txs_id
WHERE ${AND(
batchIDCondition,
"b.destination_unit = 'cashbox'",
'cit.device_id IS NOT NULL',
)}`
const recyclerBills = `SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, b.device_id
FROM empty_unit_bills b
WHERE ${AND(
deviceIDCondition,
batchIDCondition,
'b.device_id IN (SELECT device_id FROM devices WHERE paired)',
)}`
return Promise.all([db.any(cashboxBills), db.any(recyclerBills)]).then(
([cashboxBills, recyclerBills]) =>
[].concat(cashboxBills, recyclerBills).map(_.mapKeys(_.camelCase)),
)
}
module.exports = {
getBills,
}

View file

@ -0,0 +1,168 @@
const db = require('../../db')
const uuid = require('uuid')
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const {
deleteComplianceTriggersByCustomInfoRequestId,
} = require('../../compliance-triggers')
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 }))
}
// TODO: execute in a transaction
const removeCustomInfoRequest = id =>
deleteComplianceTriggersByCustomInfoRequestId(id)
.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: {
disablePermissionScreen: false,
...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,88 @@
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.load().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,23 @@
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,56 @@
const { v4: uuid } = require('uuid')
const { machineGroups, PG_ERROR_CODES } = require('typesafe-db')
const { defaultMachineGroup } = require('../../constants')
const {
ResourceAlreadyExistsError,
ResourceHasDependenciesError,
} = require('../graphql/errors')
async function getAllMachineGroups() {
return machineGroups.getMachineGroupsWithDeviceCount()
}
async function createMachineGroup(name) {
try {
const newGroup = await machineGroups.createMachineGroup({
id: uuid(),
name,
})
return {
...newGroup,
deviceCount: 0,
}
} catch (error) {
if (error.code === PG_ERROR_CODES.UNIQUE_VIOLATION) {
throw new ResourceAlreadyExistsError({ name })
}
throw error
}
}
async function deleteMachineGroup(id) {
if (id === defaultMachineGroup.uuid) {
throw new ResourceHasDependenciesError({ id, name: 'default' })
}
try {
return await machineGroups.deleteMachineGroup(id)
} catch (error) {
if (error.code === PG_ERROR_CODES.FOREIGN_KEY_VIOLATION) {
throw new ResourceHasDependenciesError({ id })
}
throw error
}
}
function assignComplianceTriggerSetToMachineGroup(id, complianceTriggerSetId) {
return machineGroups.setComplianceTriggerSetId(id, complianceTriggerSetId)
}
module.exports = {
getAllMachineGroups,
createMachineGroup,
deleteMachineGroup,
assignComplianceTriggerSetToMachineGroup,
}

View file

@ -0,0 +1,27 @@
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,37 @@
const fs = require('fs')
const pify = require('pify')
const readFile = pify(fs.readFile)
const crypto = require('crypto')
const baseX = require('base-x')
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,18 @@
const mem = require('mem')
const { machines } = require('typesafe-db')
// Cache configuration: 30 minutes
const CACHE_DURATION = 30 * 60 * 1000
const _getHighestRestrictionLevel = async () => {
return machines.getHighestRestrictionLevel()
}
const getCachedRestrictionLevel = mem(_getHighestRestrictionLevel, {
maxAge: CACHE_DURATION,
cacheKey: () => '',
})
module.exports = {
getCachedRestrictionLevel,
}

View file

@ -0,0 +1,22 @@
const _ = require('lodash/fp')
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,64 @@
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,232 @@
const _ = require('lodash/fp')
const db = require('../../db')
const BN = require('../../bn')
const { utils: coinUtils } = require('@lamassu/coins')
const {
transactions: { getTransactionById, getTransactionList },
} = require('typesafe-db')
function addProfits(txs) {
return _.map(
it => ({
...it,
profit: getProfit(it).toString(),
cryptoAmount: getCryptoAmount(it).toString(),
}),
txs,
)
}
function batch({
from = new Date(0).toISOString(),
until = new Date().toISOString(),
limit = null,
offset = 0,
txClass = null,
deviceId = null,
customerId = null,
cryptoCode = null,
toAddress = null,
status = null,
swept = null,
excludeTestingCustomers = false,
simplified,
}) {
const isCsvExport = _.isBoolean(simplified)
return getTransactionList(
{
from,
until,
cryptoCode,
txClass,
deviceId,
toAddress,
customerId,
swept,
status,
excludeTestingCustomers,
},
{ limit, offset },
)
.then(addProfits)
.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',
'profit',
'cryptoAmount',
'dispense',
'notified',
'redeem',
'phone',
'email',
'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',
'commissionPercentage',
'rawTickerPrice',
'receivedCryptoAtoms',
'discount',
'couponCode',
'txHash',
'customerPhone',
'customerEmail',
'customerIdCardDataNumber',
'customerIdCardDataExpiration',
'customerIdCardData',
'sendTime',
'customerFrontCameraPath',
'customerIdCardPhotoPath',
'expired',
'machineName',
'walletScore',
]
const addAdvancedFields = _.map(it => ({
...it,
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',
'profit',
'cryptoAmount',
]
return _.map(_.pick(fields))(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)
}
function getTx(txId) {
return getTransactionById(txId)
}
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,
getTx,
getTxAssociatedData,
updateTxCustomerPhoto,
}

View file

@ -0,0 +1,48 @@
const { PG_ERROR_CODES } = require('typesafe-db')
const complianceTriggers = require('../../compliance-triggers')
const { ResourceAlreadyExistsError } = require('../graphql/errors')
const getComplianceTriggerSets = () =>
complianceTriggers.getComplianceTriggerSets()
const getComplianceTriggerSetById = id =>
complianceTriggers.getComplianceTriggerSetById(id)
const getComplianceTriggers = complianceTriggerSetId =>
complianceTriggers.getComplianceTriggers(complianceTriggerSetId)
const createComplianceTriggerSet = name =>
complianceTriggers.createComplianceTriggerSet(name).catch(error => {
if (error.code === PG_ERROR_CODES.UNIQUE_VIOLATION)
throw new ResourceAlreadyExistsError({ name })
throw error
})
const deleteComplianceTriggerSet = id =>
complianceTriggers.deleteComplianceTriggerSet(id)
const createComplianceTrigger = (complianceTriggerSetId, trigger) =>
complianceTriggers.createComplianceTrigger(complianceTriggerSetId, trigger)
const deleteComplianceTrigger = id =>
complianceTriggers.deleteComplianceTrigger(id)
const getComplianceTriggerSetsByIdsBatch = ids =>
getComplianceTriggerSets().then(ctss => {
const ctsIdToName = Object.fromEntries(
ctss.map(({ id, name }) => [id, name]),
)
return ids.map(id => ({ id, name: ctsIdToName[id] }))
})
module.exports = {
getComplianceTriggerSets,
getComplianceTriggerSetById,
getComplianceTriggers,
createComplianceTriggerSet,
deleteComplianceTriggerSet,
createComplianceTrigger,
deleteComplianceTrigger,
getComplianceTriggerSetsByIdsBatch,
}