diff --git a/packages/admin-ui/src/Main.jsx b/packages/admin-ui/src/Main.jsx
index e162b96..4600cee 100644
--- a/packages/admin-ui/src/Main.jsx
+++ b/packages/admin-ui/src/Main.jsx
@@ -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)
diff --git a/packages/admin-ui/src/pages/Authentication/LoginState.jsx b/packages/admin-ui/src/pages/Authentication/LoginState.jsx
index ef7541c..1ba1420 100644
--- a/packages/admin-ui/src/pages/Authentication/LoginState.jsx
+++ b/packages/admin-ui/src/pages/Authentication/LoginState.jsx
@@ -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: {
diff --git a/packages/admin-ui/src/pages/Services/schemas/index.js b/packages/admin-ui/src/pages/Services/schemas/index.js
index 695aa59..f6519a9 100644
--- a/packages/admin-ui/src/pages/Services/schemas/index.js
+++ b/packages/admin-ui/src/pages/Services/schemas/index.js
@@ -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,
diff --git a/packages/admin-ui/src/pages/Services/schemas/lnbits.js b/packages/admin-ui/src/pages/Services/schemas/lnbits.js
new file mode 100644
index 0000000..00d03e5
--- /dev/null
+++ b/packages/admin-ui/src/pages/Services/schemas/lnbits.js
@@ -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)),
+ })
+ },
+}
\ No newline at end of file
diff --git a/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx b/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx
index c99295b..f46bb5f 100644
--- a/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx
+++ b/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx
@@ -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' && (
+ <>
+
Enter wallet information
+
+ >
+ )}
)
}
diff --git a/packages/server/lib/new-admin/config/accounts.js b/packages/server/lib/new-admin/config/accounts.js
index 87a3e35..e955082 100644
--- a/packages/server/lib/new-admin/config/accounts.js
+++ b/packages/server/lib/new-admin/config/accounts.js
@@ -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',
diff --git a/packages/server/lib/new-admin/graphql/modules/userManagement.js b/packages/server/lib/new-admin/graphql/modules/userManagement.js
index 492afa9..4591ed2 100644
--- a/packages/server/lib/new-admin/graphql/modules/userManagement.js
+++ b/packages/server/lib/new-admin/graphql/modules/userManagement.js
@@ -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'
})
diff --git a/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js
index 513a341..8e2f941 100644
--- a/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js
+++ b/packages/server/lib/new-admin/graphql/resolvers/users.resolver.js
@@ -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: (
diff --git a/packages/server/lib/new-admin/services/restriction-level.js b/packages/server/lib/new-admin/services/restriction-level.js
index 63196f0..7c96af6 100644
--- a/packages/server/lib/new-admin/services/restriction-level.js
+++ b/packages/server/lib/new-admin/services/restriction-level.js
@@ -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, {
diff --git a/packages/server/lib/plugins/wallet/lnbits/lnbits.js b/packages/server/lib/plugins/wallet/lnbits/lnbits.js
index e48ced7..e0fa1b1 100644
--- a/packages/server/lib/plugins/wallet/lnbits/lnbits.js
+++ b/packages/server/lib/plugins/wallet/lnbits/lnbits.js
@@ -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
}
}
diff --git a/packages/server/migrations/1750000000000-add-lnbits-config.js b/packages/server/migrations/1750000000000-add-lnbits-config.js
index 6e9c34a..351f0b1 100644
--- a/packages/server/migrations/1750000000000-add-lnbits-config.js
+++ b/packages/server/migrations/1750000000000-add-lnbits-config.js
@@ -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()
}
\ No newline at end of file
diff --git a/packages/server/package.json b/packages/server/package.json
index 696271d..d72f949 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -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",