mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-25 15:10:41 +01:00
* [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
570 lines
17 KiB
JavaScript
570 lines
17 KiB
JavaScript
/* global _, Vue, moment, LNbits, EventHub, decryptLnurlPayAES */
|
|
|
|
Vue.component('lnbits-fsat', {
|
|
props: {
|
|
amount: {
|
|
type: Number,
|
|
default: 0
|
|
}
|
|
},
|
|
template: '<span>{{ fsat }}</span>',
|
|
computed: {
|
|
fsat: function () {
|
|
return LNbits.utils.formatSat(this.amount)
|
|
}
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-wallet-list', {
|
|
data: function () {
|
|
return {
|
|
user: null,
|
|
activeWallet: null,
|
|
activeBalance: [],
|
|
showForm: false,
|
|
walletName: '',
|
|
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
|
}
|
|
},
|
|
template: `
|
|
<q-list v-if="user && user.wallets.length" dense class="lnbits-drawer__q-list">
|
|
<q-item-label header v-text="$t('wallets')"></q-item-label>
|
|
<q-item v-for="wallet in wallets" :key="wallet.id"
|
|
clickable
|
|
:active="activeWallet && activeWallet.id === wallet.id"
|
|
tag="a" :href="wallet.url">
|
|
<q-item-section side>
|
|
<q-avatar size="md"
|
|
:color="(activeWallet && activeWallet.id === wallet.id)
|
|
? (($q.dark.isActive) ? 'primary' : 'primary')
|
|
: 'grey-5'">
|
|
<q-icon name="flash_on" :size="($q.dark.isActive) ? '21px' : '20px'"
|
|
:color="($q.dark.isActive) ? 'blue-grey-10' : 'grey-3'"></q-icon>
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1">{{ wallet.name }}</q-item-label>
|
|
<q-item-label v-if="LNBITS_DENOMINATION != 'sats'" caption>{{ parseFloat(String(wallet.live_fsat).replaceAll(",", "")) / 100 }} {{ LNBITS_DENOMINATION }}</q-item-label>
|
|
<q-item-label v-else caption>{{ wallet.live_fsat }} {{ LNBITS_DENOMINATION }}</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side v-show="activeWallet && activeWallet.id === wallet.id">
|
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-item clickable @click="showForm = !showForm">
|
|
<q-item-section side>
|
|
<q-icon :name="(showForm) ? 'remove' : 'add'" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1" class="text-caption" v-text="$t('add_wallet')"></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-item v-if="showForm">
|
|
<q-item-section>
|
|
<q-form @submit="createWallet">
|
|
<q-input filled dense v-model="walletName" label="Name wallet *">
|
|
<template v-slot:append>
|
|
<q-btn round dense flat icon="send" size="sm" @click="createWallet" :disable="walletName === ''"></q-btn>
|
|
</template>
|
|
</q-input>
|
|
</q-form>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
`,
|
|
computed: {
|
|
wallets: function () {
|
|
var bal = this.activeBalance
|
|
return this.user.wallets.map(function (obj) {
|
|
obj.live_fsat =
|
|
bal.length && bal[0] === obj.id
|
|
? LNbits.utils.formatSat(bal[1])
|
|
: obj.fsat
|
|
return obj
|
|
})
|
|
}
|
|
},
|
|
methods: {
|
|
createWallet: function () {
|
|
LNbits.href.createWallet(this.walletName, this.user.id)
|
|
},
|
|
updateWalletBalance: function (payload) {
|
|
this.activeBalance = payload
|
|
}
|
|
},
|
|
created: function () {
|
|
if (window.user) {
|
|
this.user = LNbits.map.user(window.user)
|
|
}
|
|
if (window.wallet) {
|
|
this.activeWallet = LNbits.map.wallet(window.wallet)
|
|
}
|
|
EventHub.$on('update-wallet-balance', this.updateWalletBalance)
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-extension-list', {
|
|
data: function () {
|
|
return {
|
|
extensions: [],
|
|
user: null
|
|
}
|
|
},
|
|
template: `
|
|
<q-list v-if="user" dense class="lnbits-drawer__q-list">
|
|
<q-item-label header v-text="$t('extensions')"></q-item-label>
|
|
<q-item v-for="extension in userExtensions" :key="extension.code"
|
|
clickable
|
|
:active="extension.isActive"
|
|
tag="a" :href="[extension.url, '?usr=', user.id].join('')">
|
|
<q-item-section side>
|
|
<q-avatar size="md">
|
|
<q-img
|
|
:src="extension.tile"
|
|
style="max-width:20px"
|
|
></q-img>
|
|
</q-avatar>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1">{{ extension.name }} </q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side v-show="extension.isActive">
|
|
<q-icon name="chevron_right" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-item clickable tag="a" :href="['/extensions?usr=', user.id].join('')">
|
|
<q-item-section side>
|
|
<q-icon name="clear_all" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1" class="text-caption" v-text="$t('extensions')"></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<div class="lt-md q-mt-xl q-mb-xl"></div>
|
|
</q-list>
|
|
`,
|
|
computed: {
|
|
userExtensions: function () {
|
|
if (!this.user) return []
|
|
|
|
var path = window.location.pathname
|
|
var userExtensions = this.user.extensions
|
|
|
|
return this.extensions
|
|
.filter(function (obj) {
|
|
return userExtensions.indexOf(obj.code) !== -1
|
|
})
|
|
.map(function (obj) {
|
|
obj.isActive = path.startsWith(obj.url)
|
|
return obj
|
|
})
|
|
}
|
|
},
|
|
created: function () {
|
|
if (window.extensions) {
|
|
this.extensions = window.extensions
|
|
.map(function (data) {
|
|
return LNbits.map.extension(data)
|
|
})
|
|
.sort(function (a, b) {
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
}
|
|
|
|
if (window.user) {
|
|
this.user = LNbits.map.user(window.user)
|
|
}
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-admin-ui', {
|
|
props: ['showNode'],
|
|
data: function () {
|
|
return {
|
|
extensions: [],
|
|
user: null
|
|
}
|
|
},
|
|
template: `
|
|
<q-list v-if="user && user.admin" dense class="lnbits-drawer__q-list">
|
|
<q-item-label header>Admin</q-item-label>
|
|
<q-item clickable tag="a" :href="['/admin?usr=', user.id].join('')">
|
|
<q-item-section side>
|
|
<q-icon name="admin_panel_settings" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1" class="text-caption" v-text="$t('manage_server')"></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
<q-item v-if='showNode' clickable tag="a" :href="['/node?usr=', user.id].join('')">
|
|
<q-item-section side>
|
|
<q-icon name="developer_board" color="grey-5" size="md"></q-icon>
|
|
</q-item-section>
|
|
<q-item-section>
|
|
<q-item-label lines="1" class="text-caption" v-text="$t('manage_node')"></q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
`,
|
|
|
|
created: function () {
|
|
if (window.user) {
|
|
this.user = LNbits.map.user(window.user)
|
|
}
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-payment-details', {
|
|
props: ['payment'],
|
|
mixins: [windowMixin],
|
|
data: function () {
|
|
return {
|
|
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
|
}
|
|
},
|
|
template: `
|
|
<div class="q-py-md" style="text-align: left">
|
|
|
|
<div v-if="payment.tag" class="row justify-center q-mb-md">
|
|
<q-badge v-if="hasTag" color="yellow" text-color="black">
|
|
#{{ payment.tag }}
|
|
</q-badge>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<b v-text="$t('created')"></b>:
|
|
{{ payment.date }} ({{ payment.dateFrom }})
|
|
</div>
|
|
|
|
<div class="row">
|
|
<b v-text="$t('expiry')"></b>:
|
|
{{ payment.expirydate }} ({{ payment.expirydateFrom }})
|
|
</div>
|
|
|
|
<div class="row">
|
|
<b v-text="$t('amount')"></b>:
|
|
{{ (payment.amount / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
|
|
</div>
|
|
|
|
<div class="row">
|
|
<b v-text="$t('fee')"></b>:
|
|
{{ (payment.fee / 1000).toFixed(3) }} {{LNBITS_DENOMINATION}}
|
|
</div>
|
|
|
|
<div class="text-wrap">
|
|
<b style="white-space: nowrap;" v-text="$t('payment_hash')"></b>: {{ payment.payment_hash }}
|
|
<q-icon name="content_copy" @click="copyText(payment.payment_hash)" size="1em" color="grey" class="q-mb-xs cursor-pointer" />
|
|
</div>
|
|
|
|
<div class="text-wrap">
|
|
<b style="white-space: nowrap;" v-text="$t('memo')"></b>: {{ payment.memo }}
|
|
</div>
|
|
|
|
<div class="text-wrap" v-if="payment.webhook">
|
|
<b style="white-space: nowrap;" v-text="$t('webhook')"></b>: {{ payment.webhook }}: <q-badge :color="webhookStatusColor" text-color="white">
|
|
{{ webhookStatusText }}
|
|
</q-badge>
|
|
</div>
|
|
|
|
<div class="text-wrap" v-if="hasPreimage">
|
|
<b style="white-space: nowrap;" v-text="$t('payment_proof')"></b>: {{ payment.preimage }}
|
|
</div>
|
|
|
|
<div class="row" v-for="entry in extras">
|
|
<q-badge v-if="hasTag" color="secondary" text-color="white">
|
|
extra
|
|
</q-badge>
|
|
<b>{{ entry.key }}</b>:
|
|
{{ entry.value }}
|
|
</div>
|
|
|
|
<div class="row" v-if="hasSuccessAction">
|
|
<b>Success action</b>:
|
|
<lnbits-lnurlpay-success-action
|
|
:payment="payment"
|
|
:success_action="payment.extra.success_action"
|
|
></lnbits-lnurlpay-success-action>
|
|
</div>
|
|
|
|
</div>
|
|
`,
|
|
computed: {
|
|
hasPreimage() {
|
|
return (
|
|
this.payment.preimage &&
|
|
this.payment.preimage !==
|
|
'0000000000000000000000000000000000000000000000000000000000000000'
|
|
)
|
|
},
|
|
hasSuccessAction() {
|
|
return (
|
|
this.hasPreimage &&
|
|
this.payment.extra &&
|
|
this.payment.extra.success_action
|
|
)
|
|
},
|
|
webhookStatusColor() {
|
|
return this.payment.webhook_status >= 300 ||
|
|
this.payment.webhook_status < 0
|
|
? 'red-10'
|
|
: !this.payment.webhook_status
|
|
? 'cyan-7'
|
|
: 'green-10'
|
|
},
|
|
webhookStatusText() {
|
|
return this.payment.webhook_status
|
|
? this.payment.webhook_status
|
|
: 'not sent yet'
|
|
},
|
|
hasTag() {
|
|
return this.payment.extra && !!this.payment.extra.tag
|
|
},
|
|
extras() {
|
|
if (!this.payment.extra) return []
|
|
let extras = _.omit(this.payment.extra, ['tag', 'success_action'])
|
|
return Object.keys(extras).map(key => ({key, value: extras[key]}))
|
|
}
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-lnurlpay-success-action', {
|
|
props: ['payment', 'success_action'],
|
|
data() {
|
|
return {
|
|
decryptedValue: this.success_action.ciphertext
|
|
}
|
|
},
|
|
template: `
|
|
<div>
|
|
<p class="q-mb-sm">{{ success_action.message || success_action.description }}</p>
|
|
<code v-if="decryptedValue" class="text-h6 q-mt-sm q-mb-none">
|
|
{{ decryptedValue }}
|
|
</code>
|
|
<p v-else-if="success_action.url" class="text-h6 q-mt-sm q-mb-none">
|
|
<a target="_blank" style="color: inherit;" :href="success_action.url">{{ success_action.url }}</a>
|
|
</p>
|
|
</div>
|
|
`,
|
|
mounted: function () {
|
|
if (this.success_action.tag !== 'aes') return null
|
|
|
|
decryptLnurlPayAES(this.success_action, this.payment.preimage).then(
|
|
value => {
|
|
this.decryptedValue = value
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
Vue.component('lnbits-notifications-btn', {
|
|
mixins: [windowMixin],
|
|
props: ['pubkey'],
|
|
data() {
|
|
return {
|
|
isSupported: false,
|
|
isSubscribed: false,
|
|
isPermissionGranted: false,
|
|
isPermissionDenied: false
|
|
}
|
|
},
|
|
template: `
|
|
<q-btn
|
|
v-if="g.user.wallets"
|
|
:disabled="!this.isSupported"
|
|
dense
|
|
flat
|
|
round
|
|
@click="toggleNotifications()"
|
|
:icon="this.isSubscribed ? 'notifications_active' : 'notifications_off'"
|
|
size="sm"
|
|
type="a"
|
|
>
|
|
<q-tooltip v-if="this.isSupported && !this.isSubscribed">Subscribe to notifications</q-tooltip>
|
|
<q-tooltip v-if="this.isSupported && this.isSubscribed">Unsubscribe from notifications</q-tooltip>
|
|
<q-tooltip v-if="this.isSupported && this.isPermissionDenied">
|
|
Notifications are disabled,<br/>please enable or reset permissions
|
|
</q-tooltip>
|
|
<q-tooltip v-if="!this.isSupported">Notifications are not supported</q-tooltip>
|
|
</q-btn>
|
|
`,
|
|
methods: {
|
|
// converts base64 to Array buffer
|
|
urlB64ToUint8Array(base64String) {
|
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
|
const base64 = (base64String + padding)
|
|
.replace(/\-/g, '+')
|
|
.replace(/_/g, '/')
|
|
const rawData = atob(base64)
|
|
const outputArray = new Uint8Array(rawData.length)
|
|
|
|
for (let i = 0; i < rawData.length; ++i) {
|
|
outputArray[i] = rawData.charCodeAt(i)
|
|
}
|
|
|
|
return outputArray
|
|
},
|
|
toggleNotifications() {
|
|
this.isSubscribed ? this.unsubscribe() : this.subscribe()
|
|
},
|
|
saveUserSubscribed(user) {
|
|
let subscribedUsers =
|
|
JSON.parse(
|
|
this.$q.localStorage.getItem('lnbits.webpush.subscribedUsers')
|
|
) || []
|
|
if (!subscribedUsers.includes(user)) subscribedUsers.push(user)
|
|
this.$q.localStorage.set(
|
|
'lnbits.webpush.subscribedUsers',
|
|
JSON.stringify(subscribedUsers)
|
|
)
|
|
},
|
|
removeUserSubscribed(user) {
|
|
let subscribedUsers =
|
|
JSON.parse(
|
|
this.$q.localStorage.getItem('lnbits.webpush.subscribedUsers')
|
|
) || []
|
|
subscribedUsers = subscribedUsers.filter(arr => arr !== user)
|
|
this.$q.localStorage.set(
|
|
'lnbits.webpush.subscribedUsers',
|
|
JSON.stringify(subscribedUsers)
|
|
)
|
|
},
|
|
isUserSubscribed(user) {
|
|
let subscribedUsers =
|
|
JSON.parse(
|
|
this.$q.localStorage.getItem('lnbits.webpush.subscribedUsers')
|
|
) || []
|
|
return subscribedUsers.includes(user)
|
|
},
|
|
subscribe() {
|
|
var self = this
|
|
|
|
// catch clicks from disabled type='a' button (https://github.com/quasarframework/quasar/issues/9258)
|
|
if (!this.isSupported || this.isPermissionDenied) {
|
|
return
|
|
}
|
|
|
|
// ask for notification permission
|
|
Notification.requestPermission()
|
|
.then(permission => {
|
|
this.isPermissionGranted = permission === 'granted'
|
|
this.isPermissionDenied = permission === 'denied'
|
|
})
|
|
.catch(function (e) {
|
|
console.log(e)
|
|
})
|
|
|
|
// create push subscription
|
|
navigator.serviceWorker.ready.then(registration => {
|
|
navigator.serviceWorker.getRegistration().then(registration => {
|
|
registration.pushManager
|
|
.getSubscription()
|
|
.then(function (subscription) {
|
|
if (
|
|
subscription === null ||
|
|
!self.isUserSubscribed(self.g.user.id)
|
|
) {
|
|
const applicationServerKey = self.urlB64ToUint8Array(
|
|
self.pubkey
|
|
)
|
|
const options = {applicationServerKey, userVisibleOnly: true}
|
|
|
|
registration.pushManager
|
|
.subscribe(options)
|
|
.then(function (subscription) {
|
|
LNbits.api
|
|
.request(
|
|
'POST',
|
|
'/api/v1/webpush',
|
|
self.g.user.wallets[0].adminkey,
|
|
{
|
|
subscription: JSON.stringify(subscription)
|
|
}
|
|
)
|
|
.then(function (response) {
|
|
self.saveUserSubscribed(response.data.user)
|
|
self.isSubscribed = true
|
|
})
|
|
.catch(function (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
.catch(function (e) {
|
|
console.log(e)
|
|
})
|
|
})
|
|
})
|
|
},
|
|
unsubscribe() {
|
|
var self = this
|
|
|
|
navigator.serviceWorker.ready
|
|
.then(registration => {
|
|
registration.pushManager.getSubscription().then(subscription => {
|
|
if (subscription) {
|
|
LNbits.api
|
|
.request(
|
|
'DELETE',
|
|
'/api/v1/webpush?endpoint=' + btoa(subscription.endpoint),
|
|
self.g.user.wallets[0].adminkey
|
|
)
|
|
.then(function () {
|
|
self.removeUserSubscribed(self.g.user.id)
|
|
self.isSubscribed = false
|
|
})
|
|
.catch(function (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
})
|
|
}
|
|
})
|
|
})
|
|
.catch(function (e) {
|
|
console.log(e)
|
|
})
|
|
},
|
|
checkSupported: function () {
|
|
let https = window.location.protocol === 'https:'
|
|
let serviceWorkerApi = 'serviceWorker' in navigator
|
|
let notificationApi = 'Notification' in window
|
|
let pushApi = 'PushManager' in window
|
|
|
|
this.isSupported = https && serviceWorkerApi && notificationApi && pushApi
|
|
|
|
if (!this.isSupported) {
|
|
console.log(
|
|
'Notifications disabled because requirements are not met:',
|
|
{
|
|
HTTPS: https,
|
|
'Service Worker API': serviceWorkerApi,
|
|
'Notification API': notificationApi,
|
|
'Push API': pushApi
|
|
}
|
|
)
|
|
}
|
|
|
|
return this.isSupported
|
|
},
|
|
updateSubscriptionStatus: async function () {
|
|
var self = this
|
|
|
|
await navigator.serviceWorker.ready
|
|
.then(registration => {
|
|
registration.pushManager.getSubscription().then(subscription => {
|
|
self.isSubscribed =
|
|
!!subscription && self.isUserSubscribed(self.g.user.id)
|
|
})
|
|
})
|
|
.catch(function (e) {
|
|
console.log(e)
|
|
})
|
|
}
|
|
},
|
|
created: function () {
|
|
this.isPermissionDenied = Notification.permission === 'denied'
|
|
|
|
if (this.checkSupported()) {
|
|
this.updateSubscriptionStatus()
|
|
}
|
|
}
|
|
})
|