Compare commits
10 commits
eb3f1cf030
...
84e6e81f04
| Author | SHA1 | Date | |
|---|---|---|---|
| 84e6e81f04 | |||
| 41cc9f54cd | |||
| 9762a935cb | |||
| 8c5f78c50f | |||
| c96df3af1e | |||
| 69d461c5e7 | |||
| 58cb2c1565 | |||
| 9970668b95 | |||
| c5c9ff6ee8 | |||
| d3493fb2d0 |
12 changed files with 147 additions and 43 deletions
|
|
@ -29,11 +29,22 @@ const GET_USER_DATA = gql`
|
|||
|
||||
const Main = () => {
|
||||
const [location, navigate] = useLocation()
|
||||
const { wizardTested, userData, setUserData } = useContext(AppContext)
|
||||
const { wizardTested, userData, setUserData} = useContext(AppContext)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [restrictionLevel, setRestrictionLevel] = useState(null)
|
||||
|
||||
// Skip auth queries on unauthenticated pages (like /register and /login)
|
||||
const isPublicPage = location.startsWith('/register') || location.startsWith('/login')
|
||||
|
||||
// Set loading to false immediately for public pages
|
||||
React.useEffect(() => {
|
||||
if (isPublicPage) {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [isPublicPage])
|
||||
|
||||
useQuery(GET_USER_DATA, {
|
||||
skip: isPublicPage,
|
||||
onCompleted: userResponse => {
|
||||
if (!userData && userResponse?.userData) {
|
||||
setUserData(userResponse.userData)
|
||||
|
|
@ -43,6 +54,10 @@ const Main = () => {
|
|||
}
|
||||
setLoading(false)
|
||||
},
|
||||
onError: () => {
|
||||
// If query fails, just mark as not loading
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
|
||||
const sidebar = hasSidebar(location)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ const LoginState = ({ dispatch, strategy }) => {
|
|||
|
||||
if (!loginResponse.login) return
|
||||
|
||||
// Handle SKIP2FA case - directly get user data and navigate
|
||||
if (loginResponse.login === 'SKIP2FA') {
|
||||
return getUserData()
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
type: loginResponse.login,
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import inforu from './inforu'
|
|||
import infura from './infura'
|
||||
import _itbit from './itbit'
|
||||
import _kraken from './kraken'
|
||||
import lnbits from './lnbits'
|
||||
import mailgun from './mailgun'
|
||||
import scorechain from './scorechain'
|
||||
import sumsub from './sumsub'
|
||||
|
|
@ -31,6 +32,7 @@ const schemas = (markets = {}) => {
|
|||
return {
|
||||
[bitgo.code]: bitgo,
|
||||
[galoy.code]: galoy,
|
||||
[lnbits.code]: lnbits,
|
||||
[bitstamp.code]: bitstamp,
|
||||
[blockcypher.code]: blockcypher,
|
||||
[elliptic.code]: elliptic,
|
||||
|
|
|
|||
36
packages/admin-ui/src/pages/Services/schemas/lnbits.js
Normal file
36
packages/admin-ui/src/pages/Services/schemas/lnbits.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
} from '../../../components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'lnbits',
|
||||
name: 'LNBits',
|
||||
title: 'LNBits (Wallet)',
|
||||
elements: [
|
||||
{
|
||||
code: 'endpoint',
|
||||
display: 'LNBits Server URL',
|
||||
component: TextInput,
|
||||
},
|
||||
{
|
||||
code: 'adminKey',
|
||||
display: 'Admin Key',
|
||||
component: SecretInput,
|
||||
},
|
||||
],
|
||||
getValidationSchema: account => {
|
||||
return Yup.object().shape({
|
||||
endpoint: Yup.string('The endpoint must be a string')
|
||||
.max(200, 'The endpoint is too long')
|
||||
.required('The endpoint is required'),
|
||||
adminKey: Yup.string('The Admin Key must be a string')
|
||||
.max(200, 'The Admin Key is too long')
|
||||
.test(secretTest(account?.adminKey)),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ const SAVE_ACCOUNTS = gql`
|
|||
`
|
||||
|
||||
const isConfigurable = it =>
|
||||
R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
|
||||
R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy', 'lnbits'])
|
||||
|
||||
const isLocalHosted = it =>
|
||||
R.includes(it)([
|
||||
|
|
@ -178,6 +178,19 @@ const ChooseWallet = ({ data: currentData, addData }) => {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{selected === 'lnbits' && (
|
||||
<>
|
||||
<H4 noMargin>Enter wallet information</H4>
|
||||
<FormRenderer
|
||||
value={accounts.lnbits}
|
||||
save={saveWallet(selected)}
|
||||
elements={schema.lnbits.elements}
|
||||
validationSchema={schema.lnbits.getValidationSchema(accounts.lnbits)}
|
||||
buttonLabel={'Continue'}
|
||||
buttonClass={classes.formButton}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ const ALL_ACCOUNTS = [
|
|||
cryptos: [BTC, ZEC, LTC, BCH, DASH],
|
||||
},
|
||||
{ code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] },
|
||||
{ code: 'lnbits', display: 'LNBits', class: WALLET, cryptos: [LN] },
|
||||
{
|
||||
code: 'bitstamp',
|
||||
display: 'Bitstamp',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const users = require('../../../users')
|
|||
const sessionManager = require('../../../session-manager')
|
||||
const authErrors = require('../errors')
|
||||
const credentials = require('../../../hardware-credentials')
|
||||
const { skip2fa } = require('../../../environment-helper')
|
||||
|
||||
const REMEMBER_ME_AGE = 90 * T.day
|
||||
|
||||
|
|
@ -162,15 +163,25 @@ const deleteSession = (sessionID, context) => {
|
|||
return sessionManager.deleteSessionById(sessionID)
|
||||
}
|
||||
|
||||
const login = (username, password) => {
|
||||
const login = (username, password, context) => {
|
||||
return authenticateUser(username, password)
|
||||
.then(user => {
|
||||
// Skip 2FA if environment variable is set
|
||||
if (skip2fa) {
|
||||
initializeSession(context, user, false)
|
||||
return 'SKIP2FA'
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
credentials.getHardwareCredentialsByUserId(user.id),
|
||||
user.twofa_code,
|
||||
])
|
||||
})
|
||||
.then(([devices, twoFASecret]) => {
|
||||
.then(result => {
|
||||
// If we already handled skip2fa, return the result
|
||||
if (result === 'SKIP2FA') return result
|
||||
|
||||
const [devices, twoFASecret] = result
|
||||
if (!_.isEmpty(devices)) return 'FIDO'
|
||||
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -124,8 +124,8 @@ const resolver = {
|
|||
sessionManager.deleteSessionsByUsername(username),
|
||||
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) =>
|
||||
userManagement.changeUserRole(confirmationCode, id, newRole, context),
|
||||
login: (...[, { username, password }]) =>
|
||||
userManagement.login(username, password),
|
||||
login: (...[, { username, password }, context]) =>
|
||||
userManagement.login(username, password, context),
|
||||
input2FA: (...[, { username, password, rememberMe, code }, context]) =>
|
||||
userManagement.input2FA(username, password, rememberMe, code, context),
|
||||
setup2FA: (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ const { machines } = require('typesafe-db')
|
|||
const CACHE_DURATION = 30 * 60 * 1000
|
||||
|
||||
const _getHighestRestrictionLevel = async () => {
|
||||
return machines.getHighestRestrictionLevel()
|
||||
try {
|
||||
const level = await machines.getHighestRestrictionLevel()
|
||||
// Return 0 if null/undefined (no machines in database)
|
||||
return level ?? 0
|
||||
} catch (err) {
|
||||
// Log error and return 0 for empty database or other errors
|
||||
console.error('Error fetching restriction level:', err.message)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const getCachedRestrictionLevel = mem(_getHighestRestrictionLevel, {
|
||||
|
|
|
|||
|
|
@ -93,11 +93,11 @@ async function newAddress(account, info, tx) {
|
|||
const endpoint = `${account.endpoint}/api/v1/payments`
|
||||
const result = await request(endpoint, 'POST', invoiceData, account.adminKey)
|
||||
|
||||
if (!result.payment_request) {
|
||||
throw new Error('LNBits did not return a payment request')
|
||||
if (!result.bolt11) {
|
||||
throw new Error('LNBits did not return a bolt11 invoice')
|
||||
}
|
||||
|
||||
return result.payment_request
|
||||
return result.bolt11
|
||||
}
|
||||
|
||||
async function getStatus(account, tx) {
|
||||
|
|
@ -131,12 +131,44 @@ async function getStatus(account, tx) {
|
|||
}
|
||||
}
|
||||
|
||||
async function sendLNURL(account, lnurl, cryptoAtoms) {
|
||||
validateConfig(account)
|
||||
|
||||
const paymentData = {
|
||||
lnurl: lnurl,
|
||||
amount: parseInt(cryptoAtoms.toString()) * 1000, // Convert satoshis to millisatoshis
|
||||
comment: `Lamassu ATM - ${new Date().toISOString()}`
|
||||
}
|
||||
|
||||
const endpoint = `${account.endpoint}/api/v1/payments/lnurl`
|
||||
const result = await request(endpoint, 'POST', paymentData, account.adminKey)
|
||||
|
||||
if (!result.payment_hash) {
|
||||
throw new Error('LNBits LNURL payment failed: No payment hash returned')
|
||||
}
|
||||
|
||||
return {
|
||||
txid: result.payment_hash,
|
||||
fee: result.fee_msat ? Math.ceil(result.fee_msat / 1000) : 0
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCoins(account, tx) {
|
||||
validateConfig(account)
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
|
||||
await checkCryptoCode(cryptoCode)
|
||||
|
||||
// Handle LNURL addresses
|
||||
if (isLnurl(toAddress)) {
|
||||
return sendLNURL(account, toAddress, cryptoAtoms)
|
||||
}
|
||||
|
||||
// Handle bolt11 invoices
|
||||
if (!isLnInvoice(toAddress)) {
|
||||
throw new Error('Invalid Lightning address: must be bolt11 invoice or LNURL')
|
||||
}
|
||||
|
||||
const paymentData = {
|
||||
out: true,
|
||||
bolt11: toAddress
|
||||
|
|
@ -189,10 +221,9 @@ async function newFunding(account, cryptoCode) {
|
|||
const [walletBalance, fundingAddress] = await Promise.all(promises)
|
||||
|
||||
return {
|
||||
fundingAddress,
|
||||
fundingAddressQr: fundingAddress,
|
||||
confirmed: walletBalance.gte(0),
|
||||
confirmedBalance: walletBalance.toString()
|
||||
fundingPendingBalance: new BN(0),
|
||||
fundingConfirmedBalance: walletBalance,
|
||||
fundingAddress
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,17 @@
|
|||
const db = require('./db')
|
||||
const { saveConfig } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = function (next) {
|
||||
const sql = `
|
||||
INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options)
|
||||
VALUES
|
||||
('lnbitsEndpoint', 'LNBits Server URL', 'text', 'string', 'wallets', false, false, null),
|
||||
('lnbitsAdminKey', 'LNBits Admin Key', 'text', 'string', 'wallets', false, true, null)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Add LNBits as a valid wallet option for Lightning Network
|
||||
INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options)
|
||||
VALUES
|
||||
('LN_wallet', 'Lightning Network Wallet', 'text', 'string', 'wallets', true, false,
|
||||
'[{"code": "lnbits", "display": "LNBits"}, {"code": "galoy", "display": "Galoy (Blink)"}, {"code": "bitcoind", "display": "Bitcoin Core"}]')
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE SET options = EXCLUDED.options
|
||||
WHERE user_config.options NOT LIKE '%lnbits%';
|
||||
`
|
||||
const config = {
|
||||
'lnbits_endpoint': '',
|
||||
'lnbits_adminKey': '',
|
||||
'LN_wallet': 'lnbits'
|
||||
}
|
||||
|
||||
db.multi(sql, next)
|
||||
saveConfig(config).then(next).catch(next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
const sql = `
|
||||
DELETE FROM user_config
|
||||
WHERE name IN ('lnbitsEndpoint', 'lnbitsAdminKey');
|
||||
|
||||
-- Remove LNBits from wallet options
|
||||
UPDATE user_config
|
||||
SET options = REPLACE(options, ', {"code": "lnbits", "display": "LNBits"}', '')
|
||||
WHERE name = 'LN_wallet';
|
||||
`
|
||||
|
||||
db.multi(sql, next)
|
||||
// No-op - removing config entries is not typically done in down migrations
|
||||
// as it could break existing configurations
|
||||
next()
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
"bchaddrjs": "^0.3.0",
|
||||
"bignumber.js": "9.0.1",
|
||||
"bip39": "^2.3.1",
|
||||
"bolt11": "^1.4.1",
|
||||
"ccxt": "2.9.16",
|
||||
"compression": "^1.7.4",
|
||||
"connect-pg-simple": "^6.2.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue