diff --git a/__init__.py b/__init__.py index d0cafc3..d7f2a3b 100644 --- a/__init__.py +++ b/__init__.py @@ -32,7 +32,6 @@ def nwcprovider_stop(): except Exception as ex: logger.warning(ex) -logger.debug("NWC Service Provider extension loaded") def nwcprovider_start(): task = create_permanent_unique_task("ext_nwcprovider", handle_nwc) scheduled_tasks.append(task) diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..e650761 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,386 @@ + window.app = Vue.createApp({ + el: "#vue", + mixins: [windowMixin], + delimiters: ["${", "}"], + data: function () { + return { + selectedWallet: null, + nodePermissions: [], + nwcEntries: [], + nwcsTable: { + columns: [ + { + name: "description", + align: "left", + label: "Description", + field: "description", + }, + { name: "status", align: "left", label: "Status", field: "status" }, + { + name: "last_used", + align: "left", + label: "Last used", + field: "last_used", + }, + { + name: "created_at", + align: "left", + label: "Created", + field: "created_at", + }, + { + name: "expires_at", + align: "left", + label: "Expires", + field: "expires_at", + }, + ], + pagination: { + rowsPerPage: 10, + }, + }, + connectDialog: { + show: false, + data: {}, + }, + pairingDialog: { + show: false, + data: { + pairingUrl: "", + }, + }, + pairingQrDialog: { + show: false, + data: { + pairingUrl: "", + }, + }, + connectionInfoDialog: { + show: false, + data: {}, + }, + }; + }, + + methods: { + showConnectDialog() { + const wallet = this.getWallet(); + if (!wallet) { + this.$q.notify({ + color: "red", + message: "Please select a wallet first", + }); + return; + } else { + this.connectDialog.show = true; + } + }, + openConnectionInfoDialog(data) { + this.connectionInfoDialog.data = data; + this.connectionInfoDialog.show = true; + }, + closeConnectionInfoDialog() { + this.connectionInfoDialog.show = false; + }, + openPairingUrl() { + const url = this.pairingDialog.data.pairingUrl; + if (url) window.open(url, "_blank"); + }, + go(url) { + window.open(url, "_blank"); + }, + async copyPairingUrl() { + const url = this.pairingDialog.data.pairingUrl; + if (url) { + try { + await navigator.clipboard.writeText(url); + this.$q.notify({ + color: "primary", + message: "URL copied to clipboard!", + }); + } catch (err) { + this.$q.notify({ color: "red", message: "Failed to copy URL." }); + } + } + }, + showPairingQR() { + this.pairingQrDialog.data.pairingUrl = + this.pairingDialog.data.pairingUrl; + this.pairingQrDialog.show = true; + }, + closePairingQrDialog() { + this.pairingQrDialog.show = false; + }, + loadConnectDialogData() { + this.connectDialog.data = { + description: "", + expires_at: Date.now() + 1000 * 60 * 60 * 24 * 7, + neverExpires: true, + permissions: [], + budgets: [], + }; + for (const permission of this.nodePermissions) { + this.connectDialog.data.permissions.push({ + key: permission.key, + name: permission.name, + value: permission.value, + }); + } + }, + deleteBudget(index) { + this.connectDialog.data.budgets.splice(index, 1); + }, + addBudget() { + this.connectDialog.data.budgets.push({ + budget_sats: 1000, + used_budget_sats: 0, + created_at: + new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000, + expiration: "never", + }); + }, + closeConnectDialog() { + this.connectDialog.show = false; + this.loadConnectDialogData(); + }, + getWallet: function () { + let wallet = undefined; + for (let i = 0; i < this.g.user.wallets.length; i++) { + if (this.g.user.wallets[i].id == this.selectedWallet) { + wallet = this.g.user.wallets[i]; + break; + } + } + return wallet; + }, + async generateKeyPair() { + while (!window.NobleSecp256k1) { + await new Promise((resolve) => setTimeout(resolve, 1)); + } + const privKeyBytes = window.NobleSecp256k1.utils.randomPrivateKey(); + const pubKeyBytes = window.NobleSecp256k1.getPublicKey(privKeyBytes); + const out = { + privKeyBytes: privKeyBytes, + pubKeyBytes: pubKeyBytes, + privKey: window.NobleSecp256k1.etc.bytesToHex(privKeyBytes), + pubKey: window.NobleSecp256k1.etc.bytesToHex(pubKeyBytes.slice(1)), + }; + return out; + }, + deleteNWC: async function (pubkey) { + this.$q + .dialog({ + title: "Confirm Deletion", + message: "Are you sure you want to delete this connection?", + cancel: true, + persistent: true, + }) + .onOk(async () => { + try { + const wallet = this.getWallet(); + const response = await LNbits.api.request( + "DELETE", + `/nwcprovider/api/v1/nwc/${pubkey}`, + wallet.adminkey, + ); + this.loadNwcs(); + this.$q.notify({ + type: "positive", + message: "Deleted successfully", + }); + } catch (error) { + LNbits.utils.notifyApiError(error); + } + }) + .onCancel(() => { + // User canceled the operation + }); + }, + loadNwcs: async function () { + const wallet = this.getWallet(); + if (!wallet) { + this.nwcs = []; + return; + } + try { + const response = await LNbits.api.request( + "GET", + "/nwcprovider/api/v1/nwc?include_expired=true&calculate_spent_budget=true", + wallet.adminkey, + ); + this.nwcs = response.data; + } catch (error) { + this.nwcs = []; + } + try { + const response = await LNbits.api.request( + "GET", + "/nwcprovider/api/v1/permissions", + wallet.adminkey, + ); + const permissions = []; + for (const [key, value] of Object.entries(response.data)) { + permissions.push({ + key: key, + name: value.name, + value: value.default, + }); + } + this.nodePermissions = permissions; + } catch (error) { + Lnbits.utils.notifyApiError(error); + } + this.loadConnectDialogData(); + const newTableEntries = []; + for (const nwc of this.nwcs) { + const t = Quasar.utils.date.formatDate( + new Date(nwc.data.created_at * 1000), + "YYYY-MM-DD HH:mm", + ); + const e = + nwc.data.expires_at > 0 + ? Quasar.utils.date.formatDate( + new Date(nwc.data.expires_at * 1000), + "YYYY-MM-DD HH:mm", + ) + : "Never"; + const l = Quasar.utils.date.formatDate( + new Date(nwc.data.last_used * 1000), + "YYYY-MM-DD HH:mm", + ); + const nwcTableEntry = { + description: nwc.data.description, + created_at: t, + expires_at: e, + last_used: l, + pubkey: nwc.data.pubkey, + permissions: nwc.data.permissions, + budgets: [], + status: "Active", + }; + if ( + nwc.data.expires_at > 0 && + nwc.data.expires_at < new Date().getTime() / 1000 + ) { + nwcTableEntry.status = "Expired"; + } + for (const budget of nwc.budgets) { + const createdAt = Quasar.utils.date.formatDate( + new Date(budget.created_at * 1000), + "YYYY-MM-DD HH:mm", + ); + let refreshWindow = budget.refresh_window; + if (refreshWindow <= 0) { + refreshWindow = "Never"; + } else if (refreshWindow == 60 * 60 * 24) { + refreshWindow = "Daily"; + } else if (refreshWindow == 60 * 60 * 24 * 7) { + refreshWindow = "Weekly"; + } else if (refreshWindow == 60 * 60 * 24 * 30) { + refreshWindow = "Monthly"; + } else if (refreshWindow == 60 * 60 * 24 * 365) { + refreshWindow = "Yearly"; + } + nwcTableEntry.budgets.push({ + budget_sats: budget.budget_msats / 1000, + used_budget_sats: budget.used_budget_msats / 1000, + created_at: createdAt, + refresh_window: refreshWindow, + }); + } + newTableEntries.push(nwcTableEntry); + } + this.nwcEntries = newTableEntries; + }, + closePairingDialog() { + this.pairingDialog.show = false; + }, + async showPairingDialog(secret) { + let response = await LNbits.api.request( + "GET", + "/nwcprovider/api/v1/pairing/{SECRET}", + ); + response = response.data; + response = response.replace("{SECRET}", secret); + this.pairingDialog.data.pairingUrl = response; + this.pairingDialog.show = true; + }, + async confirmConnectDialog() { + const keyPair = await this.generateKeyPair(); + // timestamp + let expires_at = 0; + if (!this.connectDialog.data.neverExpires) { + expires_at = + new Date(this.connectDialog.data.expires_at).getTime() / 1000; + } + const data = { + permissions: [], + description: this.connectDialog.data.description, + expires_at: expires_at, + budgets: [], + }; + for (const permission of this.connectDialog.data.permissions) { + if (permission.value) data.permissions.push(permission.key); + } + for (const budget of this.connectDialog.data.budgets) { + const budget_msats = budget.budget_sats * 1000; + let refresh_window = 0; + switch (budget.expiry) { + case "Daily": + refresh_window = 60 * 60 * 24; + break; + case "Weekly": + refresh_window = 60 * 60 * 24 * 7; + break; + case "Monthly": + refresh_window = 60 * 60 * 24 * 30; + break; + case "Yearly": + refresh_window = 60 * 60 * 24 * 365; + break; + case "Never": + refresh_window = 0; + break; + } + data.budgets.push({ + budget_msats: budget_msats, + refresh_window: refresh_window, + created_at: + new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000, + }); + } + const wallet = this.getWallet(); + + try { + const response = await LNbits.api.request( + "PUT", + "/nwcprovider/api/v1/nwc/" + keyPair.pubKey, + wallet.adminkey, + data, + ); + this.closeConnectDialog(); + if ( + !response.data || + !response.data.data || + !response.data.data.pubkey + ) { + LNbits.utils.notifyApiError("Error creating nwc pairing"); + return; + } + this.showPairingDialog(keyPair.privKey); + } catch (error) { + LNbits.utils.notifyApiError(error); + } + this.loadNwcs(); + }, + }, + + created: function () { + this.loadNwcs(); + }, + watch: { + selectedWallet(newValue, oldValue) { + this.loadNwcs(); + }, + }, + }); \ No newline at end of file diff --git a/templates/nwcprovider/index.html b/templates/nwcprovider/index.html index 819e254..143c7af 100644 --- a/templates/nwcprovider/index.html +++ b/templates/nwcprovider/index.html @@ -1,9 +1,11 @@ - - - - {% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} +%} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} {% block page %}