Compare commits

...

10 commits

Author SHA1 Message Date
84e6e81f04 fix: handle empty machines table in restrictionLevel query
Return 0 when no machines exist instead of throwing an error.
This fixes the authentication error on fresh installations where
the machines table is empty.
2025-12-31 19:05:01 +01:00
41cc9f54cd fix: skip auth queries on public pages (register/login)
The GET_USER_DATA query includes restrictionLevel which has @auth directive.
This was causing authentication errors on /register and /login pages where
users are not yet authenticated.

Solution:
- Skip the GraphQL query when on public pages (/register or /login)
- Use useEffect to set loading=false immediately for public pages
- Add onError handler to gracefully handle query failures

Fixes authentication error: 'Message: Authentication failed, Path: restrictionLevel'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-31 19:05:01 +01:00
9762a935cb add 2fa bypass 2025-12-31 19:05:01 +01:00
8c5f78c50f fix: correct LNBits newFunding return format for funding page
Updates the newFunding function to return the expected interface:
- fundingPendingBalance: BN(0) for Lightning Network
- fundingConfirmedBalance: actual wallet balance as BN object
- fundingAddress: bolt11 invoice for funding

This fixes the TypeError "Cannot read properties of undefined (reading
'minus')"
that occurred when accessing the funding page in the admin UI.
2025-12-31 19:05:01 +01:00
c96df3af1e fix: update LNBits payment request handling
- Changed the response handling in the newAddress function to return the bolt11 invoice instead of the payment request.
- Updated error message to reflect the change in response structure from LNBits.
2025-12-31 19:05:01 +01:00
69d461c5e7 feat: implement LNURL payment handling in LNBits plugin
- Added a new function to handle LNURL payments, allowing users to send payments via LNURL addresses.
- Integrated LNURL payment processing into the existing sendCoins function, enhancing the wallet's capabilities for Lightning Network transactions.
2025-12-31 19:05:01 +01:00
58cb2c1565 feat: add bolt11 library for Lightning Network invoice handling
- Included the bolt11 library in the server package to facilitate the creation and parsing of Lightning Network invoices.
2025-12-31 19:05:01 +01:00
9970668b95 feat: integrate LNBits wallet schema and configuration
- Added LNBits wallet schema to the admin UI, including validation and input components.
- Updated the services index to include LNBits in the available wallet options.
- Enhanced the wallet selection component to handle LNBits configuration input.
2025-12-31 19:05:01 +01:00
c5c9ff6ee8 refactor: update LNBits migration to use configuration object
- Replaced SQL statements with a configuration object for LNBits settings, enhancing code clarity and maintainability.
- Simplified the migration process by utilizing the saveConfig function for applying configurations.
- Marked the down migration as a no-op to prevent breaking existing configurations.
2025-12-31 19:05:01 +01:00
d3493fb2d0 refactor: streamline LNBits migration SQL statements
- Updated the migration script for LNBits configuration to use an array for SQL statements, improving readability and maintainability.
- Consolidated the insertion and deletion operations for user configuration related to LNBits and Lightning Network wallet options.
2025-12-31 19:05:01 +01:00
12 changed files with 147 additions and 43 deletions

View file

@ -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)

View file

@ -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: {

View file

@ -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,

View 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)),
})
},
}

View file

@ -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>
)
}

View file

@ -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',

View file

@ -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'
})

View file

@ -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: (

View file

@ -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, {

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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",