Generate validation schema for LNbits

This commit is contained in:
ekzyis 2024-07-17 00:04:35 +02:00
parent 9587ff9a52
commit fb2b34ce67
3 changed files with 83 additions and 33 deletions

View File

@ -10,7 +10,7 @@ import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './
import * as usersFragments from '@/fragments/users' import * as usersFragments from '@/fragments/users'
import * as subsFragments from '@/fragments/subs' import * as subsFragments from '@/fragments/subs'
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon' import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
import { TOR_REGEXP, parseNwcUrl } from './url' import { parseNwcUrl } from './url'
import { datePivot } from './time' import { datePivot } from './time'
import { decodeRune } from '@/lib/cln' import { decodeRune } from '@/lib/cln'
import bip39Words from './bip39-words' import bip39Words from './bip39-words'
@ -600,31 +600,6 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
return accum return accum
}, {}))) }, {})))
export const lnbitsSchema = object({
url: process.env.NODE_ENV === 'development'
? string()
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
.required('required').trim()
: string().url().required('required').trim()
.test(async (url, context) => {
if (TOR_REGEXP.test(url)) {
// allow HTTP and HTTPS over Tor
if (!/^https?:\/\//.test(url)) {
return context.createError({ message: 'http or https required' })
}
return true
}
try {
// force HTTPS over clearnet
await string().https().validate(url)
} catch (err) {
return context.createError({ message: err.message })
}
return true
}),
adminKey: string().length(32)
})
export const nwcSchema = object({ export const nwcSchema = object({
nwcUrl: string() nwcUrl: string()
.required('required') .required('required')

View File

@ -10,6 +10,10 @@ import Info from '@/components/info'
import Text from '@/components/text' import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared' import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { object, string } from 'yup'
import { autowithdrawSchemaMembers } from '@/lib/validate'
import { useMe } from '@/components/me'
import { TOR_REGEXP } from '@/lib/url'
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false }) const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
@ -20,6 +24,7 @@ export default function WalletSettings () {
const router = useRouter() const router = useRouter()
const { wallet: name } = router.query const { wallet: name } = router.query
const wallet = useWallet(name) const wallet = useWallet(name)
const me = useMe()
const initial = wallet.fields.reduce((acc, field) => { const initial = wallet.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce // We still need to run over all wallet fields via reduce
@ -33,6 +38,8 @@ export default function WalletSettings () {
} }
}, wallet.config) }, wallet.config)
const schema = generateSchema(wallet, { me })
return ( return (
<CenterLayout> <CenterLayout>
<h2 className='pb-2'>{wallet.card.title}</h2> <h2 className='pb-2'>{wallet.card.title}</h2>
@ -40,7 +47,7 @@ export default function WalletSettings () {
{!wallet.walletType && <WalletSecurityBanner />} {!wallet.walletType && <WalletSecurityBanner />}
<Form <Form
initial={initial} initial={initial}
schema={wallet.schema} schema={schema}
onSubmit={async (values) => { onSubmit={async (values) => {
try { try {
const newConfig = !wallet.isConfigured const newConfig = !wallet.isConfigured
@ -130,3 +137,67 @@ function WalletFields ({ wallet: { config, fields } }) {
return null return null
}) })
} }
function generateSchema (wallet, { me }) {
if (wallet.schema) return wallet.schema
const fieldValidator = (field) => {
if (!field.validate) {
// default validation
let validator = string()
if (!field.optional) validator = validator.required('required')
return validator
}
const { type: validationType } = field.validate
let validator
const stringTypes = ['url', 'string']
if (stringTypes.includes(validationType)) {
validator = string()
if (field.validate.length) {
validator = validator.length(field.validate.length)
}
}
if (validationType === 'url') {
validator = process.env.NODE_ENV === 'development'
? validator
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
: validator
.url()
.test(async (url, context) => {
if (field.validate.onionAllowed && TOR_REGEXP.test(url)) {
// allow HTTP and HTTPS over Tor
if (!/^https?:\/\//.test(url)) {
return context.createError({ message: 'http or https required' })
}
return true
}
try {
// force HTTPS over clearnet
await string().https().validate(url)
} catch (err) {
return context.createError({ message: err.message })
}
return true
})
}
if (!field.optional) validator = validator.required('required')
return validator
}
return object(
wallet.fields.reduce((acc, field) => {
return {
...acc,
[field.name]: fieldValidator(field)
}
}, wallet.walletType ? autowithdrawSchemaMembers({ me }) : {})
)
}

View File

@ -1,17 +1,23 @@
import { lnbitsSchema } from '@/lib/validate'
export const name = 'lnbits' export const name = 'lnbits'
export const fields = [ export const fields = [
{ {
name: 'url', name: 'url',
label: 'lnbits url', label: 'lnbits url',
type: 'text' type: 'text',
validate: {
type: 'url',
onionAllowed: true
}
}, },
{ {
name: 'adminKey', name: 'adminKey',
label: 'admin key', label: 'admin key',
type: 'password' type: 'password',
validate: {
type: 'string',
length: 32
}
} }
] ]
@ -20,5 +26,3 @@ export const card = {
subtitle: 'use [LNbits](https://lnbits.com/) for payments', subtitle: 'use [LNbits](https://lnbits.com/) for payments',
badges: ['send only', 'non-custodialish'] badges: ['send only', 'non-custodialish']
} }
export const schema = lnbitsSchema