Compare commits
90
Commits
v1.3.1
..
fix/labels
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2320018302 | ||
|
|
0dc0725a9b | ||
|
|
8c77f75cf1 | ||
|
|
21505471d5 | ||
|
|
baa9a35773 | ||
|
|
7f114ddcc0 | ||
|
|
92aad20dd7 | ||
|
|
0f4ae5da86 | ||
|
|
7a796c6510 | ||
|
|
0910687328 | ||
|
|
33e2fc2ea8 | ||
|
|
0c6e8394c8 | ||
|
|
d55e2a0e1f | ||
|
|
148ba9d275 | ||
|
|
d2ca774f6f | ||
|
|
233398b512 | ||
|
|
152c1dbb74 | ||
|
|
44985cb0d1 | ||
|
|
b1a7692ce4 | ||
|
|
31bff37b1e | ||
|
|
3f7a1798d5 | ||
|
|
98d4dbab8b | ||
|
|
7c7a04da9d | ||
|
|
3ccefb70fa | ||
|
|
5346ac47b1 | ||
|
|
9f4ed53888 | ||
|
|
faac56c14c | ||
|
|
3e0d0d9896 | ||
|
|
b9a004a5e4 | ||
|
|
da3cb548bd | ||
|
|
4144359617 | ||
|
|
07268b87d2 | ||
|
|
c356874bcb | ||
|
|
f9b5c7db7a | ||
|
|
20f9ddb57c | ||
|
|
ee57ba4c1a | ||
|
|
ff7f5c1ca5 | ||
|
|
1463d75ee2 | ||
|
|
f1fc4710ee | ||
|
|
00eaec8290 | ||
|
|
28c4235c94 | ||
|
|
b7d178c08e | ||
|
|
9d0ec97d39 | ||
|
|
b6f533be24 | ||
|
|
5e36bb4fcd | ||
|
|
f9b0604711 | ||
|
|
39c33699af | ||
|
|
c89721223f | ||
|
|
1e6e97c12d | ||
|
|
783efb19db | ||
|
|
9ffc63b5dc | ||
|
|
0ec8139e5c | ||
|
|
a8c3181852 | ||
|
|
e038ceb9be | ||
|
|
b4eccb9e5d | ||
|
|
691b7ad055 | ||
|
|
c84d1b66c6 | ||
|
|
cf7726ddfd | ||
|
|
bd50a3c546 | ||
|
|
c9270bbf7f | ||
|
|
6474aeb982 | ||
|
|
37ba437ad1 | ||
|
|
35af181e24 | ||
|
|
f8c58aef0e | ||
|
|
8755984bd8 | ||
|
|
0d5661cda7 | ||
|
|
c2a3fbc6c0 | ||
|
|
6bac34fc6b | ||
|
|
b54eedee84 | ||
|
|
bd07f7a5ef | ||
|
|
a40306f5cd | ||
|
|
2fecec2623 | ||
|
|
4ad89396cc | ||
|
|
82307c4839 | ||
|
|
8506199c2f | ||
|
|
d142d76148 | ||
|
|
98e9a61b98 | ||
|
|
877af0c21e | ||
|
|
dd27b190f3 | ||
|
|
2856803ca7 | ||
|
|
ca8264b1f5 | ||
|
|
39ca9da870 | ||
|
|
2b603bdc48 | ||
|
|
26780df065 | ||
|
|
248fcc06ab | ||
|
|
f794373d30 | ||
|
|
87f25ea715 | ||
|
|
b9de754598 | ||
|
|
785fb7af8e | ||
|
|
55c5ab3a6d |
@@ -2,7 +2,26 @@ name: Build LNbits AppImage
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'The tag name for the release'
|
||||
required: true
|
||||
type: string
|
||||
upload_url:
|
||||
description: 'The upload URL for the release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: 'The tag name for the release'
|
||||
required: true
|
||||
type: string
|
||||
upload_url:
|
||||
description: 'The upload URL for the release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-linux-package:
|
||||
@@ -75,7 +94,7 @@ jobs:
|
||||
# Build AppImage
|
||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x appimagetool-x86_64.AppImage
|
||||
TAG_NAME=${{ github.event.release.tag_name }}
|
||||
TAG_NAME=${{ inputs.tag_name }}
|
||||
APPIMAGE_NAME="LNbits-${TAG_NAME}.AppImage"
|
||||
./appimagetool-x86_64.AppImage \
|
||||
--updateinformation "gh-releases-zsync|lnbits|lnbits|latest|*.AppImage.zsync" \
|
||||
@@ -93,7 +112,7 @@ jobs:
|
||||
- name: Upload Linux Release Asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
upload_url: ${{ inputs.upload_url }}
|
||||
asset_path: ${{ env.APPIMAGE_NAME }}
|
||||
asset_name: ${{ env.APPIMAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
@@ -13,6 +13,8 @@ jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create github release
|
||||
@@ -20,7 +22,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: |
|
||||
gh release create "$tag" --generate-notes --draft
|
||||
upload_url=$(gh release create "$tag" --generate-notes --draft --json upload_url -q '.upload_url')
|
||||
echo "upload_url=$upload_url" >> "$GITHUB_OUTPUT"
|
||||
|
||||
docker:
|
||||
needs: [ release ]
|
||||
@@ -56,3 +59,6 @@ jobs:
|
||||
appimage:
|
||||
needs: [ release ]
|
||||
uses: ./.github/workflows/appimage.yml
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img width="2000" height="203" alt="lnbits_head" src="https://github.com/user-attachments/assets/77669718-ac10-43c7-ae95-6ce236c77401" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||
|
||||
@@ -77,13 +78,11 @@ As well as working great in a browser, LNbits has native IoS and Android apps as
|
||||
|
||||
LNbits empowers everyone with modular, open-source tools for building Bitcoin-based systems — fast, free, and extendable.
|
||||
|
||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/)
|
||||
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||
|
||||
@@ -4,6 +4,7 @@ color_scheme: dark
|
||||
logo: "/logos/lnbits-full-inverse.png"
|
||||
search_enabled: true
|
||||
url: https://docs.lnbits.org
|
||||
markdown: gfm
|
||||
aux_links:
|
||||
"LNbits on GitHub":
|
||||
- "//github.com/lnbits/lnbits"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: FastAPI extension upgrade
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
## Defining a route with path parameters
|
||||
|
||||
**old:**
|
||||
+34
-22
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Admin UI
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
@@ -12,11 +18,13 @@
|
||||
|
||||
# LNBits Admin UI
|
||||
|
||||
We introduced the Admin UI as the new default to make setup simpler and more straightforward. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
|
||||
[What you can do](#what-you-can-do-with-the-admin-ui) · [First Run](#first-run-and-super-user-id) · [Enable/Disable](#enabling-or-disabling-the-admin-ui) · [Reset](#reset-to-defaults) · [Allowed Users](#allowed-users) · [Guides](#additional-guides)
|
||||
|
||||
**We introduced the Admin UI as the new default to make setup simpler and more straightforward**. Instead of hand editing the `.env` file, you configure key server settings directly in the frontend with clear labels and guardrails.
|
||||
|
||||
<ins>On a fresh install the Admin UI is enabled by default</ins>, and at first launch you are prompted to create **Super User** credentials so that sensitive operations, such as switching funding sources, remain in trusted hands. When the Admin UI is enabled, configuration is written to and read from the database; for all settings managed by the UI, the parameters in `.env` are largely no longer used. If you disable the Admin UI, the `.env` file becomes the single source of truth again.
|
||||
|
||||
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
|
||||
For privileged actions and role details see **[Super User](./super_user.md)** & [User Roles](./user_roles.md)
|
||||
For a complete reference of legacy variables consult **[.env.example](../../.env.example)**.
|
||||
|
||||
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
|
||||
@@ -36,6 +44,27 @@ For a complete reference of legacy variables consult **[.env.example](../../.env
|
||||
> [!NOTE]
|
||||
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
|
||||
|
||||
## First run and Super User ID
|
||||
|
||||
On first start with the Admin UI enabled you will be prompted to generate a Super User.
|
||||
|
||||
<img width="1573" height="976" alt="Admin_UI_first_install" src="https://github.com/user-attachments/assets/05aa634f-06ec-4a4d-a5c6-d90927c90991" />
|
||||
|
||||
If you need to read it from disk later:
|
||||
|
||||
```bash
|
||||
cat /lnbits/data/.super_user
|
||||
# example
|
||||
123de4bfdddddbbeb48c8bc8382fe123
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
|
||||
|
||||
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
|
||||
|
||||
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
|
||||
|
||||
## Enabling or disabling the Admin UI
|
||||
|
||||
The Admin UI is enabled by default on new installs. To change the state:
|
||||
@@ -76,23 +105,6 @@ The Admin UI is enabled by default on new installs. To change the state:
|
||||
|
||||
Using `Reset to defaults` in the Admin UI wipes stored settings. After a restart, a new `Super User` is created and the old one is no longer valid.
|
||||
|
||||
## First run and Super User ID
|
||||
|
||||
On first start with the Admin UI enabled you will be prompted to generate a Super User. If you need to read it from disk later:
|
||||
|
||||
```bash
|
||||
cat /lnbits/data/.super_user
|
||||
# example
|
||||
123de4bfdddddbbeb48c8bc8382fe123
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> For security reasons, Super Users and Admin users must authenticate with credentials (username and password).
|
||||
|
||||
After login you will see **Settings** and **Users** in the sidebar between **Wallets** and **Extensions**, plus a role badge in the top left.
|
||||
|
||||
<img width="1353" height="914" alt="grafik" src="https://github.com/user-attachments/assets/06bb4f36-a23a-4058-87ec-60440d322c25" />
|
||||
|
||||
## Allowed Users
|
||||
|
||||
When set **at least one**, LNbits becomes private: only the listed users and Admins can access the frontend. Account creation is disabled automatically. You can also disable account creation explicitly.
|
||||
@@ -105,8 +117,8 @@ When set **at least one**, LNbits becomes private: only the listed users and Adm
|
||||
## Additional Guides
|
||||
|
||||
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
||||
- **[User Roles](./User_Roles.md)** — Overview of existing roles in LNbits.
|
||||
- **[Funding sources](./funding-sources_table.md)** — What is available and how to configure each.
|
||||
- **[User Roles](./user_roles.md)** — Overview of existing roles in LNbits.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What is available and how to configure each.
|
||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||
|
||||
## Powered by LNbits
|
||||
@@ -115,7 +127,7 @@ LNbits empowers everyone with modular, open source tools for building Bitcoin ba
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Extension Install
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Extension Install
|
||||
|
||||
Anyone can create an extension by following the [example extension](https://github.com/lnbits/example) and [making extensions](https://github.com/lnbits/lnbits/blob/main/docs/devs/extensions.md) dev guide.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Wallet comparison
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
@@ -72,7 +78,7 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
|
||||
+33
-27
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Installation
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
@@ -51,8 +57,8 @@ LNBITS_ADMIN_UI=true HOST=0.0.0.0 PORT=5000 ./LNbits-latest.AppImage # most syst
|
||||
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||
|
||||
> [!NOTE]
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
## Option 2: UV (recommended for developers)
|
||||
@@ -117,8 +123,8 @@ uv run lnbits
|
||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||
|
||||
> [!NOTE]
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
||||
@@ -197,8 +203,8 @@ poetry run lnbits
|
||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
@@ -217,8 +223,8 @@ chmod +x lnbits.sh &&
|
||||
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
@@ -286,7 +292,7 @@ SUPER_USER=be54db7f245346c8833eaa430e1e0405 LNBITS_ADMIN_UI=true ./result/bin/ln
|
||||
```
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> **Next steps**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
@@ -340,8 +346,8 @@ docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
||||
```
|
||||
|
||||
> 
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
</details>
|
||||
@@ -380,14 +386,14 @@ You'll be prompted to enter an app name, region, postgres (choose no), deploy no
|
||||
You'll now find a file in the directory called `fly.toml`. Open that file and modify/add the following settings.
|
||||
|
||||
> 
|
||||
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
|
||||
> Environment variable strings should be quoted here. For example, if `.env` has
|
||||
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
|
||||
> Be sure to replace `${PUT_YOUR_LNBITS_ENV_VARS_HERE}` with all relevant environment variables in `.env` or `.env.example`.
|
||||
> Environment variable strings should be quoted here. For example, if `.env` has
|
||||
> `LNBITS_ENDPOINT=https://demo.lnbits.com`, then in `fly.toml` use
|
||||
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
|
||||
|
||||
> 
|
||||
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
|
||||
> Example (LND REST funding source):
|
||||
> Don't enter secret environment variables here. Fly.io offers **secrets** (via `fly secrets`) that are exposed as env vars at runtime.
|
||||
> Example (LND REST funding source):
|
||||
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
||||
|
||||
```
|
||||
@@ -445,8 +451,8 @@ poetry add setuptools wheel
|
||||
|
||||
> 
|
||||
>
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> **Next steps**
|
||||
> Install complete → **[Running LNbits](#run-the-server)**
|
||||
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||
|
||||
## Troubleshooting
|
||||
@@ -524,22 +530,22 @@ Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
|
||||
> [!NOTE]
|
||||
> **Paths overview**
|
||||
>
|
||||
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
|
||||
> - **SuperUser file:** `<lnbits_root>/data/.super_user`
|
||||
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
|
||||
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
||||
> - **Docker:** bind a host directory to `/app/data`.
|
||||
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
||||
> - **Docker:** bind a host directory to `/app/data`.
|
||||
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
||||
> The container reads `/app/.env` (usually bind-mounted from your project root).
|
||||
|
||||
> [!TIP]
|
||||
> **Local Lightning test network**
|
||||
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
||||
> **Local Lightning test network**
|
||||
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
||||
> https://lightningpolar.com/
|
||||
|
||||
> [!TIP]
|
||||
> **API comparison before updates**
|
||||
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
|
||||
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
||||
> **API comparison before updates**
|
||||
> Use **TableTown** to diff your LNbits instance against another (dev vs prod) or the upstream dev branch. Spot endpoint changes before updating.
|
||||
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
||||
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
||||
|
||||
# Additional guides
|
||||
@@ -884,5 +890,5 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
||||
|
||||
If you like this project [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visiting our [Shop](https://shop.lnbits.com)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/) [](https://my.lnbits.com/login) [](https://news.lnbits.com/) [](https://extensions.lnbits.com/)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Super user
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
@@ -110,8 +116,8 @@ These are practical tips for running a safe and friendly instance.
|
||||
## Additional guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
|
||||
- **[User Roles](./User_Roles.md)** — Overview of roles and what they can do.
|
||||
- **[Funding sources](./funding-sources_table.md)** — Available options and how to enable and configure them.
|
||||
- **[User Roles](./user_roles.md)** — Overview of roles and what they can do.
|
||||
- **[Funding sources](./funding-sources-table.md)** — Available options and how to enable and configure them.
|
||||
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: User Roles
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||
@@ -14,7 +20,7 @@
|
||||
|
||||
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
|
||||
|
||||
**Jump to:**
|
||||
**Jump to:**
|
||||
[Roles at a Glance](#roles-at-a-glance) •
|
||||
[Super User](#super-user--master-control) •
|
||||
[Admin](#admin--day-to-day-manager) •
|
||||
@@ -81,7 +87,7 @@ For using LNbits, not administering it.
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||
- **[Super User](./super_user.md)** — Deep dive on responsibilities and safe usage patterns.
|
||||
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
@@ -89,7 +95,7 @@ LNbits empowers everyone with modular, open-source tools for building Bitcoin-ba
|
||||
|
||||
If you like this project, [send some tip love](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg) or visit our [Shop](https://shop.lnbits.de)
|
||||
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://shop.lnbits.com/)
|
||||
[](https://my.lnbits.com/login)
|
||||
[](https://news.lnbits.com/)
|
||||
|
||||
@@ -354,8 +354,8 @@ Get more info here [https://docs.strike.me/strike-oauth-connect/](https://docs.s
|
||||
## Additional Guides
|
||||
|
||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
||||
- **[User Roles](./User_Roles.md)** — Quick Overview of existing Roles in LNBits.
|
||||
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
||||
- **[User Roles](./user_roles.md)** — Quick Overview of existing Roles in LNBits.
|
||||
- **[Funding sources](./funding-sources-table.md)** — What’s available and how to enable/configure each.
|
||||
|
||||
## Powered by LNbits
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
nixpkgs.overlays = [ self.overlays.${system}.default ];
|
||||
};
|
||||
|
||||
checks = { };
|
||||
checks =
|
||||
import ./nix/tests { inherit pkgs; flake = self; };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from fastapi import APIRouter, FastAPI
|
||||
from .db import core_app_extra, db
|
||||
from .views.admin_api import admin_router
|
||||
from .views.api import api_router
|
||||
from .views.asset_api import asset_router
|
||||
from .views.audit_api import audit_router
|
||||
from .views.auth_api import auth_router
|
||||
from .views.callback_api import callback_router
|
||||
@@ -44,6 +45,7 @@ def init_core_routers(app: FastAPI):
|
||||
app.include_router(webpush_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(audit_router)
|
||||
app.include_router(asset_router)
|
||||
app.include_router(fiat_router)
|
||||
app.include_router(lnurl_router)
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.assets import Asset, AssetFilters, AssetInfo
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
|
||||
|
||||
async def create_asset(
|
||||
entry: Asset,
|
||||
conn: Connection | None = None,
|
||||
) -> None:
|
||||
await (conn or db).insert("assets", entry)
|
||||
|
||||
|
||||
async def get_user_asset_info(
|
||||
user_id: str,
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def get_asset_info(
|
||||
asset_id: str, conn: Connection | None = None
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id",
|
||||
values={"asset_id": asset_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_asset(
|
||||
user_id: str,
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Asset | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
model=Asset,
|
||||
)
|
||||
|
||||
|
||||
async def get_public_asset(
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Asset | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||
values={"asset_id": asset_id},
|
||||
model=Asset,
|
||||
)
|
||||
|
||||
|
||||
async def get_public_asset_info(
|
||||
asset_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> AssetInfo | None:
|
||||
return await (conn or db).fetchone(
|
||||
query="SELECT * from assets WHERE id = :asset_id AND is_public = true",
|
||||
values={"asset_id": asset_id},
|
||||
model=AssetInfo,
|
||||
)
|
||||
|
||||
|
||||
async def update_user_asset_info(
|
||||
asset: AssetInfo,
|
||||
) -> AssetInfo:
|
||||
await db.update("assets", asset)
|
||||
return asset
|
||||
|
||||
|
||||
async def delete_user_asset(
|
||||
user_id: str, asset_id: str, conn: Connection | None = None
|
||||
) -> None:
|
||||
await (conn or db).execute(
|
||||
query="DELETE FROM assets WHERE id = :asset_id AND user_id = :user_id",
|
||||
values={"asset_id": asset_id, "user_id": user_id},
|
||||
)
|
||||
|
||||
|
||||
async def get_user_assets(
|
||||
user_id: str,
|
||||
filters: Filters[AssetFilters] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AssetInfo]:
|
||||
filters = filters or Filters()
|
||||
filters.sortby = filters.sortby or "created_at"
|
||||
return await (conn or db).fetch_page(
|
||||
query="SELECT * FROM assets",
|
||||
where=["user_id = :user_id"],
|
||||
values={"user_id": user_id},
|
||||
filters=filters,
|
||||
model=AssetInfo,
|
||||
table_name="assets",
|
||||
)
|
||||
|
||||
|
||||
async def get_user_assets_count(user_id: str) -> int:
|
||||
result = await db.execute(
|
||||
query="SELECT COUNT(*) as count FROM assets WHERE user_id = :user_id",
|
||||
values={"user_id": user_id},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
return row.get("count", 0)
|
||||
@@ -16,11 +16,12 @@ async def get_audit_entries(
|
||||
conn: Connection | None = None,
|
||||
) -> Page[AuditEntry]:
|
||||
return await (conn or db).fetch_page(
|
||||
"SELECT * from audit",
|
||||
"SELECT * FROM audit",
|
||||
[],
|
||||
{},
|
||||
filters=filters,
|
||||
model=AuditEntry,
|
||||
table_name="audit",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ async def get_standalone_payment(
|
||||
) -> Payment | None:
|
||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||
values = {
|
||||
"wallet_id": wallet_id,
|
||||
"checking_id": checking_id_or_hash,
|
||||
"hash": checking_id_or_hash,
|
||||
}
|
||||
@@ -47,6 +46,10 @@ async def get_standalone_payment(
|
||||
clause = f"({clause}) AND amount > 0"
|
||||
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
@@ -66,13 +69,16 @@ async def get_standalone_payment(
|
||||
async def get_wallet_payment(
|
||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||
) -> Payment | None:
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return None
|
||||
payment = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE wallet_id = :wallet AND payment_hash = :hash
|
||||
""",
|
||||
{"wallet": wallet_id, "hash": payment_hash},
|
||||
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
|
||||
Payment,
|
||||
)
|
||||
return payment
|
||||
@@ -128,7 +134,11 @@ async def get_payments_paginated( # noqa: C901
|
||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||
|
||||
if wallet_id:
|
||||
values["wallet_id"] = wallet_id
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return Page(data=[], total=0)
|
||||
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
clause.append("wallet_id = :wallet_id")
|
||||
elif user_id:
|
||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||
@@ -171,6 +181,7 @@ async def get_payments_paginated( # noqa: C901
|
||||
values,
|
||||
filters=filters,
|
||||
model=Payment,
|
||||
table_name="apipayments",
|
||||
)
|
||||
|
||||
|
||||
@@ -282,6 +293,7 @@ async def create_payment(
|
||||
fee=-abs(data.fee),
|
||||
tag=extra.get("tag", None),
|
||||
extra=extra,
|
||||
labels=data.labels or [],
|
||||
)
|
||||
|
||||
await (conn or db).insert("apipayments", payment)
|
||||
@@ -320,7 +332,7 @@ async def get_payments_history(
|
||||
|
||||
date_trunc = db.datetime_grouping(group)
|
||||
|
||||
values = {
|
||||
values: dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
}
|
||||
# count outgoing payments if they are still pending
|
||||
@@ -350,10 +362,10 @@ async def get_payments_history(
|
||||
)
|
||||
if wallet_id:
|
||||
wallet = await get_wallet(wallet_id)
|
||||
if wallet:
|
||||
balance = wallet.balance_msat
|
||||
else:
|
||||
raise ValueError("Unknown wallet")
|
||||
if not wallet or not wallet.can_view_payments:
|
||||
return []
|
||||
balance = wallet.balance_msat
|
||||
values["wallet_id"] = wallet.source_wallet_id
|
||||
else:
|
||||
balance = await get_total_balance()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||
from lnbits.core.crud.wallets import get_wallets
|
||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import UserAcls
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
@@ -89,6 +89,7 @@ async def get_accounts(
|
||||
filters=filters,
|
||||
model=AccountOverview,
|
||||
group_by=["accounts.id"],
|
||||
table_name="accounts",
|
||||
)
|
||||
|
||||
|
||||
@@ -179,8 +180,13 @@ async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
||||
async def get_user_from_account(
|
||||
account: Account, conn: Connection | None = None
|
||||
) -> User | None:
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||
wallets = await get_wallets(account.id, False, conn=conn)
|
||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
||||
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
||||
|
||||
if len(wallets) == 0:
|
||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
||||
wallets.append(wallet)
|
||||
|
||||
return User(
|
||||
id=account.id,
|
||||
email=account.email,
|
||||
|
||||
+81
-13
@@ -3,7 +3,7 @@ from time import time
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.wallets import WalletsFilters
|
||||
from lnbits.core.models.wallets import WalletsFilters, WalletType
|
||||
from lnbits.db import Connection, Filters, Page
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -14,17 +14,22 @@ async def create_wallet(
|
||||
*,
|
||||
user_id: str,
|
||||
wallet_name: str | None = None,
|
||||
wallet_type: WalletType = WalletType.LIGHTNING,
|
||||
shared_wallet_id: str | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
wallet_id = uuid4().hex
|
||||
wallet = Wallet(
|
||||
id=wallet_id,
|
||||
name=wallet_name or settings.lnbits_default_wallet_name,
|
||||
wallet_type=wallet_type.value,
|
||||
shared_wallet_id=shared_wallet_id,
|
||||
user=user_id,
|
||||
adminkey=uuid4().hex,
|
||||
inkey=uuid4().hex,
|
||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||
)
|
||||
|
||||
await (conn or db).insert("wallets", wallet)
|
||||
return wallet
|
||||
|
||||
@@ -103,8 +108,8 @@ async def delete_unused_wallets(
|
||||
)
|
||||
|
||||
|
||||
async def get_wallet(
|
||||
wallet_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
async def get_standalone_wallet(
|
||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
@@ -121,8 +126,23 @@ async def get_wallet(
|
||||
)
|
||||
|
||||
|
||||
async def get_wallet(
|
||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
wallet = await get_standalone_wallet(wallet_id, deleted, conn)
|
||||
if not wallet:
|
||||
return None
|
||||
if wallet.is_lightning_shared_wallet:
|
||||
return await get_source_wallet(wallet, conn)
|
||||
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_wallets(
|
||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
user_id: str,
|
||||
deleted: bool | None = False,
|
||||
wallet_type: WalletType | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> list[Wallet]:
|
||||
query = """
|
||||
SELECT *, COALESCE((
|
||||
@@ -132,12 +152,20 @@ async def get_wallets(
|
||||
"""
|
||||
if deleted is not None:
|
||||
query += " AND deleted = :deleted "
|
||||
return await (conn or db).fetchall(
|
||||
if wallet_type is not None:
|
||||
query += " AND wallet_type = :wallet_type "
|
||||
wallets = await (conn or db).fetchall(
|
||||
query,
|
||||
{"user": user_id, "deleted": deleted},
|
||||
{
|
||||
"user": user_id,
|
||||
"deleted": deleted,
|
||||
"wallet_type": wallet_type.value if wallet_type else None,
|
||||
},
|
||||
Wallet,
|
||||
)
|
||||
|
||||
return await get_source_wallets(wallets, conn)
|
||||
|
||||
|
||||
async def get_wallets_paginated(
|
||||
user_id: str,
|
||||
@@ -149,7 +177,7 @@ async def get_wallets_paginated(
|
||||
deleted = False
|
||||
|
||||
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||
return await (conn or db).fetch_page(
|
||||
wallets = await (conn or db).fetch_page(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
@@ -159,20 +187,27 @@ async def get_wallets_paginated(
|
||||
values={"user": user_id, "deleted": deleted},
|
||||
filters=filters,
|
||||
model=Wallet,
|
||||
table_name="wallets",
|
||||
)
|
||||
|
||||
wallets.data = await get_source_wallets(wallets.data, conn)
|
||||
return wallets
|
||||
|
||||
|
||||
async def get_wallets_ids(
|
||||
user_id: str, deleted: bool | None = None, conn: Connection | None = None
|
||||
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||
) -> list[str]:
|
||||
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
||||
query = """SELECT * FROM wallets WHERE "user" = :user"""
|
||||
if deleted is not None:
|
||||
query += "AND deleted = :deleted"
|
||||
result: list[dict] = await (conn or db).fetchall(
|
||||
query += " AND deleted = :deleted "
|
||||
wallets = await (conn or db).fetchall(
|
||||
query,
|
||||
{"user": user_id, "deleted": deleted},
|
||||
Wallet,
|
||||
)
|
||||
return [row["id"] for row in result]
|
||||
|
||||
wallets = await get_source_wallets(wallets, conn)
|
||||
return [w.source_wallet_id for w in wallets if w.can_view_payments]
|
||||
|
||||
|
||||
async def get_wallets_count():
|
||||
@@ -185,7 +220,7 @@ async def get_wallet_for_key(
|
||||
key: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet | None:
|
||||
return await (conn or db).fetchone(
|
||||
wallet = await (conn or db).fetchone(
|
||||
"""
|
||||
SELECT *, COALESCE((
|
||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||
@@ -196,6 +231,39 @@ async def get_wallet_for_key(
|
||||
{"key": key},
|
||||
Wallet,
|
||||
)
|
||||
if not wallet:
|
||||
return None
|
||||
|
||||
if wallet.is_lightning_shared_wallet:
|
||||
mw = await get_source_wallet(wallet, conn)
|
||||
return mw
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_source_wallet(
|
||||
wallet: Wallet, conn: Connection | None = None
|
||||
) -> Wallet | None:
|
||||
if not wallet.is_lightning_shared_wallet:
|
||||
return wallet
|
||||
if not wallet.shared_wallet_id:
|
||||
return None
|
||||
|
||||
shared_wallet = await get_standalone_wallet(wallet.shared_wallet_id, False, conn)
|
||||
if not shared_wallet:
|
||||
return None
|
||||
wallet.mirror_shared_wallet(shared_wallet)
|
||||
return wallet
|
||||
|
||||
|
||||
async def get_source_wallets(
|
||||
wallet: list[Wallet], conn: Connection | None = None
|
||||
) -> list[Wallet]:
|
||||
source_wallets = []
|
||||
for w in wallet:
|
||||
source_wallet = await get_source_wallet(w, conn)
|
||||
if source_wallet:
|
||||
source_wallets.append(source_wallet)
|
||||
return source_wallets
|
||||
|
||||
|
||||
async def get_total_balance(conn: Connection | None = None):
|
||||
|
||||
@@ -743,3 +743,46 @@ async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m035_add_wallet_type_column(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
ALTER TABLE wallets ADD COLUMN wallet_type TEXT DEFAULT 'lightning'
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m036_add_shared_wallet_column(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
ALTER TABLE wallets ADD COLUMN shared_wallet_id TEXT
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m037_create_assets_table(db: Connection):
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
is_public BOOLEAN NOT NULL DEFAULT false,
|
||||
name TEXT NOT NULL,
|
||||
size_bytes INT NOT NULL,
|
||||
thumbnail_base64 TEXT,
|
||||
thumbnail {db.blob},
|
||||
data {db.blob} NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m038_add_labels_for_payments(db: Connection):
|
||||
await db.execute(
|
||||
"""
|
||||
ALTER TABLE apipayments ADD COLUMN labels TEXT
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from lnbits.db import FilterModel
|
||||
|
||||
|
||||
class AssetInfo(BaseModel):
|
||||
id: str
|
||||
mime_type: str
|
||||
name: str
|
||||
is_public: bool = False
|
||||
size_bytes: int
|
||||
thumbnail_base64: str | None = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class Asset(AssetInfo):
|
||||
user_id: str
|
||||
data: bytes
|
||||
|
||||
|
||||
class AssetUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
is_public: bool | None = None
|
||||
|
||||
|
||||
class AssetFilters(FilterModel):
|
||||
__search_fields__ = ["name"]
|
||||
__sort_fields__ = [
|
||||
"created_at",
|
||||
"name",
|
||||
]
|
||||
|
||||
name: str | None = None
|
||||
@@ -59,6 +59,7 @@ class CreatePayment(BaseModel):
|
||||
expiry: datetime | None = None
|
||||
webhook: str | None = None
|
||||
fee: int = 0
|
||||
labels: list[str] | None = None
|
||||
|
||||
|
||||
class Payment(BaseModel):
|
||||
@@ -81,6 +82,7 @@ class Payment(BaseModel):
|
||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
labels: list[str] = []
|
||||
extra: dict = {}
|
||||
|
||||
def __init__(self, **data):
|
||||
@@ -184,7 +186,15 @@ class Payment(BaseModel):
|
||||
|
||||
|
||||
class PaymentFilters(FilterModel):
|
||||
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
||||
__search_fields__ = [
|
||||
"memo",
|
||||
"amount",
|
||||
"wallet_id",
|
||||
"tag",
|
||||
"status",
|
||||
"time",
|
||||
"labels",
|
||||
]
|
||||
|
||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||
|
||||
@@ -198,6 +208,7 @@ class PaymentFilters(FilterModel):
|
||||
preimage: str | None
|
||||
payment_hash: str | None
|
||||
wallet_id: str | None
|
||||
labels: str | None
|
||||
|
||||
|
||||
class PaymentDataPoint(BaseModel):
|
||||
@@ -272,6 +283,7 @@ class CreateInvoice(BaseModel):
|
||||
bolt11: str | None = None
|
||||
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||
fiat_provider: str | None = None
|
||||
labels: list[str] = []
|
||||
|
||||
@validator("payment_hash")
|
||||
def check_hex(cls, v):
|
||||
@@ -320,3 +332,7 @@ class CancelInvoice(BaseModel):
|
||||
def check_hex(cls, v):
|
||||
_ = bytes.fromhex(v)
|
||||
return v
|
||||
|
||||
|
||||
class UpdatePaymentLabels(BaseModel):
|
||||
labels: list[str] = []
|
||||
|
||||
@@ -12,6 +12,7 @@ from lnbits.db import FilterModel
|
||||
from lnbits.helpers import (
|
||||
is_valid_email_address,
|
||||
is_valid_external_id,
|
||||
is_valid_label,
|
||||
is_valid_pubkey,
|
||||
is_valid_username,
|
||||
)
|
||||
@@ -29,6 +30,21 @@ class UserNotifications(BaseModel):
|
||||
incoming_payments_sats: int = 0
|
||||
|
||||
|
||||
class WalletInviteRequest(BaseModel):
|
||||
request_id: str
|
||||
from_user_name: str | None = None
|
||||
to_wallet_id: str
|
||||
to_wallet_name: str
|
||||
|
||||
|
||||
class UserLabel(BaseModel):
|
||||
name: str = Field(regex=r"([A-Za-z0-9 ._-]{1,100}$)")
|
||||
description: str | None = Field(default=None, max_length=250)
|
||||
color: str | None = Field(
|
||||
default=None, regex=r"^#[0-9A-Fa-f]{6}$"
|
||||
) # e.g., "#RRGGBB"
|
||||
|
||||
|
||||
class UserExtra(BaseModel):
|
||||
email_verified: bool | None = False
|
||||
first_name: str | None = None
|
||||
@@ -46,6 +62,55 @@ class UserExtra(BaseModel):
|
||||
|
||||
notifications: UserNotifications = UserNotifications()
|
||||
|
||||
wallet_invite_requests: list[WalletInviteRequest] = []
|
||||
|
||||
labels: list[UserLabel] = []
|
||||
|
||||
def add_wallet_invite_request(
|
||||
self,
|
||||
request_id: str,
|
||||
to_wallet_id: str,
|
||||
to_wallet_name: str,
|
||||
from_user_name: str | None = None,
|
||||
) -> WalletInviteRequest:
|
||||
self.remove_wallet_invite_request(request_id)
|
||||
invite = WalletInviteRequest(
|
||||
request_id=request_id,
|
||||
from_user_name=from_user_name,
|
||||
to_wallet_id=to_wallet_id,
|
||||
to_wallet_name=to_wallet_name,
|
||||
)
|
||||
self.wallet_invite_requests.append(invite)
|
||||
return invite
|
||||
|
||||
def find_wallet_invite_request(self, request_id: str) -> WalletInviteRequest | None:
|
||||
for invite in self.wallet_invite_requests:
|
||||
if invite.request_id == request_id:
|
||||
return invite
|
||||
return None
|
||||
|
||||
def validate_labels(self):
|
||||
seen_labels = set()
|
||||
for label in self.labels:
|
||||
if not label.name:
|
||||
raise ValueError("Label name cannot be empty.")
|
||||
# apply the same rule for labels as for usernames
|
||||
if not is_valid_label(label.name):
|
||||
raise ValueError(f"Invalid label name: {label.name}")
|
||||
if label.name in seen_labels:
|
||||
raise ValueError(f"Duplicate label name: {label.name}")
|
||||
seen_labels.add(label.name)
|
||||
|
||||
def remove_wallet_invite_request(
|
||||
self,
|
||||
request_id: str,
|
||||
):
|
||||
self.wallet_invite_requests = [
|
||||
invite
|
||||
for invite in self.wallet_invite_requests
|
||||
if invite.request_id != request_id
|
||||
]
|
||||
|
||||
|
||||
class EndpointAccess(BaseModel):
|
||||
path: str
|
||||
@@ -160,6 +225,8 @@ class Account(BaseModel):
|
||||
if user_uuid4.hex != self.id:
|
||||
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||
|
||||
self.extra.validate_labels()
|
||||
|
||||
|
||||
class AccountOverview(Account):
|
||||
transaction_count: int | None = 0
|
||||
@@ -170,7 +237,7 @@ class AccountOverview(Account):
|
||||
|
||||
class AccountFilters(FilterModel):
|
||||
__search_fields__ = [
|
||||
"user",
|
||||
"id",
|
||||
"email",
|
||||
"username",
|
||||
"pubkey",
|
||||
@@ -178,17 +245,18 @@ class AccountFilters(FilterModel):
|
||||
"wallet_id",
|
||||
]
|
||||
__sort_fields__ = [
|
||||
"balance_msat",
|
||||
"id",
|
||||
"email",
|
||||
"username",
|
||||
"transaction_count",
|
||||
"wallet_count",
|
||||
"last_payment",
|
||||
"pubkey",
|
||||
"external_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
email: str | None = None
|
||||
user: str | None = None
|
||||
id: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
pubkey: str | None = None
|
||||
external_id: str | None = None
|
||||
wallet_id: str | None = None
|
||||
|
||||
@@ -4,12 +4,10 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
|
||||
from lnurl import encode as lnurl_encode
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from lnbits.core.models.lnurl import StoredPayLinks
|
||||
from lnbits.db import FilterModel
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
@@ -21,10 +19,95 @@ class BaseWallet(BaseModel):
|
||||
balance_msat: int
|
||||
|
||||
|
||||
class WalletType(Enum):
|
||||
LIGHTNING = "lightning"
|
||||
LIGHTNING_SHARED = "lightning-shared"
|
||||
|
||||
|
||||
class WalletPermission(Enum):
|
||||
VIEW_PAYMENTS = "view-payments"
|
||||
RECEIVE_PAYMENTS = "receive-payments"
|
||||
SEND_PAYMENTS = "send-payments"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class WalletShareStatus(Enum):
|
||||
INVITE_SENT = "invite_sent"
|
||||
APPROVED = "approved"
|
||||
|
||||
|
||||
class WalletSharePermission(BaseModel):
|
||||
# unique identifier for this share request
|
||||
request_id: str | None = None
|
||||
# username of the invited user
|
||||
username: str
|
||||
# ID of the wallet being shared with
|
||||
shared_with_wallet_id: str | None = None
|
||||
# permissions being granted
|
||||
permissions: list[WalletPermission] = []
|
||||
# status of the share request
|
||||
status: WalletShareStatus
|
||||
comment: str | None = None
|
||||
|
||||
def approve(
|
||||
self,
|
||||
permissions: list[WalletPermission] | None = None,
|
||||
shared_with_wallet_id: str | None = None,
|
||||
):
|
||||
self.status = WalletShareStatus.APPROVED
|
||||
if permissions is not None:
|
||||
self.permissions = permissions
|
||||
if shared_with_wallet_id is not None:
|
||||
self.shared_with_wallet_id = shared_with_wallet_id
|
||||
|
||||
@property
|
||||
def is_approved(self) -> bool:
|
||||
return self.status == WalletShareStatus.APPROVED
|
||||
|
||||
|
||||
class WalletExtra(BaseModel):
|
||||
icon: str = "flash_on"
|
||||
color: str = "primary"
|
||||
pinned: bool = False
|
||||
# What permissions this wallet grants when it's shared with other users
|
||||
shared_with: list[WalletSharePermission] = []
|
||||
|
||||
def invite_user_to_shared_wallet(
|
||||
self,
|
||||
request_id: str,
|
||||
request_type: WalletShareStatus,
|
||||
username: str,
|
||||
permissions: list[WalletPermission] | None = None,
|
||||
) -> WalletSharePermission:
|
||||
share = WalletSharePermission(
|
||||
request_id=request_id,
|
||||
username=username,
|
||||
status=request_type,
|
||||
permissions=permissions or [],
|
||||
)
|
||||
self.shared_with.append(share)
|
||||
return share
|
||||
|
||||
def find_share_by_id(self, request_id: str) -> WalletSharePermission | None:
|
||||
for share in self.shared_with:
|
||||
if share.request_id == request_id:
|
||||
return share
|
||||
return None
|
||||
|
||||
def find_share_for_wallet(
|
||||
self, shared_with_wallet_id: str
|
||||
) -> WalletSharePermission | None:
|
||||
for share in self.shared_with:
|
||||
if share.shared_with_wallet_id == shared_with_wallet_id:
|
||||
return share
|
||||
return None
|
||||
|
||||
def remove_share_by_id(self, request_id: str):
|
||||
self.shared_with = [
|
||||
share for share in self.shared_with if share.request_id != request_id
|
||||
]
|
||||
|
||||
|
||||
class Wallet(BaseModel):
|
||||
@@ -33,6 +116,9 @@ class Wallet(BaseModel):
|
||||
name: str
|
||||
adminkey: str
|
||||
inkey: str
|
||||
wallet_type: str = WalletType.LIGHTNING.value
|
||||
# Must be set only for shared wallets
|
||||
shared_wallet_id: str | None = None
|
||||
deleted: bool = False
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
@@ -40,6 +126,65 @@ class Wallet(BaseModel):
|
||||
balance_msat: int = Field(default=0, no_database=True)
|
||||
extra: WalletExtra = WalletExtra()
|
||||
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
||||
# What permission this wallet has when it's a shared wallet
|
||||
share_permissions: list[WalletPermission] = Field(default=[], no_database=True)
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
self._validate_data()
|
||||
|
||||
def mirror_shared_wallet(
|
||||
self,
|
||||
shared_wallet: Wallet,
|
||||
):
|
||||
if not shared_wallet.is_lightning_wallet:
|
||||
return None
|
||||
|
||||
self.wallet_type = WalletType.LIGHTNING_SHARED.value
|
||||
self.shared_wallet_id = shared_wallet.id
|
||||
self.name = shared_wallet.name
|
||||
self.share_permissions = shared_wallet.get_share_permissions(self.id)
|
||||
|
||||
if len(self.share_permissions):
|
||||
self.currency = shared_wallet.currency
|
||||
self.balance_msat = shared_wallet.balance_msat
|
||||
|
||||
self.stored_paylinks = shared_wallet.stored_paylinks
|
||||
self.extra.icon = shared_wallet.extra.icon
|
||||
self.extra.color = shared_wallet.extra.color
|
||||
|
||||
def get_share_permissions(self, wallet_id: str) -> list[WalletPermission]:
|
||||
for share in self.extra.shared_with:
|
||||
if share.shared_with_wallet_id == wallet_id and share.is_approved:
|
||||
return share.permissions
|
||||
return []
|
||||
|
||||
def has_permission(self, permission: WalletPermission) -> bool:
|
||||
if self.is_lightning_wallet:
|
||||
return True
|
||||
if self.is_lightning_shared_wallet:
|
||||
return permission in self.share_permissions
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def source_wallet_id(self) -> str:
|
||||
"""For shared wallets return the original wallet ID, else return own ID."""
|
||||
if self.is_lightning_shared_wallet and len(self.share_permissions):
|
||||
return self.shared_wallet_id or self.id
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def can_receive_payments(self) -> bool:
|
||||
return self.has_permission(WalletPermission.RECEIVE_PAYMENTS)
|
||||
|
||||
@property
|
||||
def can_send_payments(self) -> bool:
|
||||
return self.has_permission(WalletPermission.SEND_PAYMENTS)
|
||||
|
||||
@property
|
||||
def can_view_payments(self) -> bool:
|
||||
return self.has_permission(WalletPermission.VIEW_PAYMENTS)
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
@@ -50,16 +195,23 @@ class Wallet(BaseModel):
|
||||
return self.balance_msat - settings.fee_reserve(self.balance_msat)
|
||||
|
||||
@property
|
||||
def lnurlwithdraw_full(self) -> str:
|
||||
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
||||
try:
|
||||
return lnurl_encode(url)
|
||||
except Exception:
|
||||
return ""
|
||||
def is_lightning_wallet(self) -> bool:
|
||||
return self.wallet_type == WalletType.LIGHTNING.value
|
||||
|
||||
@property
|
||||
def is_lightning_shared_wallet(self) -> bool:
|
||||
return self.wallet_type == WalletType.LIGHTNING_SHARED.value
|
||||
|
||||
def _validate_data(self):
|
||||
if self.is_lightning_shared_wallet:
|
||||
if not self.shared_wallet_id:
|
||||
raise ValueError("Shared wallet ID must be set for shared wallets.")
|
||||
|
||||
|
||||
class CreateWallet(BaseModel):
|
||||
name: str | None = None
|
||||
wallet_type: WalletType = WalletType.LIGHTNING
|
||||
shared_wallet_id: str | None = None
|
||||
|
||||
|
||||
class KeyType(Enum):
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import base64
|
||||
import io
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import UploadFile
|
||||
from PIL import Image
|
||||
|
||||
from lnbits.core.crud.assets import create_asset, get_user_assets_count
|
||||
from lnbits.core.models.assets import Asset
|
||||
from lnbits.settings import settings
|
||||
|
||||
|
||||
async def create_user_asset(user_id: str, file: UploadFile, is_public: bool) -> Asset:
|
||||
if not file.content_type:
|
||||
raise ValueError("File must have a content type.")
|
||||
if file.content_type.lower() not in settings.lnbits_assets_allowed_mime_types:
|
||||
raise ValueError(f"File type '{file.content_type}' not allowed.")
|
||||
|
||||
if user_id not in settings.lnbits_assets_no_limit_users:
|
||||
user_assets_count = await get_user_assets_count(user_id)
|
||||
if user_assets_count >= settings.lnbits_max_assets_per_user:
|
||||
raise ValueError(
|
||||
f"Max upload count of {settings.lnbits_max_assets_per_user} exceeded."
|
||||
)
|
||||
|
||||
contents = await file.read()
|
||||
if len(contents) > settings.lnbits_max_asset_size_mb * 1024 * 1024:
|
||||
raise ValueError(
|
||||
f"File limit of {settings.lnbits_max_asset_size_mb}MB exceeded."
|
||||
)
|
||||
|
||||
image = Image.open(io.BytesIO(contents))
|
||||
|
||||
thumbnail_width = min(256, settings.lnbits_asset_thumbnail_width)
|
||||
thumbnail_height = min(256, settings.lnbits_asset_thumbnail_height)
|
||||
image.thumbnail((thumbnail_width, thumbnail_height))
|
||||
|
||||
# Save thumbnail to an in-memory buffer
|
||||
thumb_buffer = io.BytesIO()
|
||||
thumbnail_format = settings.lnbits_asset_thumbnail_format or "PNG"
|
||||
image.save(thumb_buffer, format=thumbnail_format)
|
||||
thumb_buffer.seek(0)
|
||||
|
||||
asset = Asset(
|
||||
id=uuid4().hex,
|
||||
user_id=user_id,
|
||||
mime_type=file.content_type,
|
||||
is_public=is_public,
|
||||
name=file.filename or "unnamed",
|
||||
size_bytes=len(contents),
|
||||
thumbnail_base64=base64.b64encode(thumb_buffer.getvalue()).decode("utf-8"),
|
||||
data=contents,
|
||||
)
|
||||
|
||||
await create_asset(asset)
|
||||
return asset
|
||||
@@ -70,7 +70,8 @@ async def check_balance_delta_changed():
|
||||
if settings.latest_balance_delta_sats is None:
|
||||
settings.latest_balance_delta_sats = status.delta_sats
|
||||
return
|
||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||
delta_change = abs(status.delta_sats - settings.latest_balance_delta_sats)
|
||||
if delta_change >= settings.notification_balance_delta_threshold_sats:
|
||||
enqueue_admin_notification(
|
||||
NotificationType.balance_delta,
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ from lnbits.core.crud import (
|
||||
mark_webhook_sent,
|
||||
)
|
||||
from lnbits.core.crud.users import get_user
|
||||
from lnbits.core.crud.wallets import get_wallet
|
||||
from lnbits.core.models import Payment, Wallet
|
||||
from lnbits.core.models.notifications import (
|
||||
NOTIFICATION_TEMPLATES,
|
||||
@@ -240,6 +241,10 @@ async def dispatch_webhook(payment: Payment):
|
||||
async with httpx.AsyncClient(headers=headers) as client:
|
||||
try:
|
||||
check_callback_url(payment.webhook)
|
||||
except ValueError as exc:
|
||||
await mark_webhook_sent(payment.payment_hash, "-1")
|
||||
logger.warning(f"Invalid webhook URL {payment.webhook}: {exc!s}")
|
||||
try:
|
||||
r = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
||||
r.raise_for_status()
|
||||
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
||||
@@ -257,6 +262,12 @@ async def dispatch_webhook(payment: Payment):
|
||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
try:
|
||||
await send_ws_payment_notification(wallet, payment)
|
||||
for shared in wallet.extra.shared_with:
|
||||
if not shared.shared_with_wallet_id:
|
||||
continue
|
||||
shared_wallet = await get_wallet(shared.shared_with_wallet_id)
|
||||
if shared_wallet and shared_wallet.can_view_payments:
|
||||
await send_ws_payment_notification(shared_wallet, payment)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||
try:
|
||||
@@ -381,7 +392,7 @@ def _is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||
if message_type == NotificationType.watchdog_check:
|
||||
return settings.lnbits_notification_watchdog
|
||||
if message_type == NotificationType.balance_delta:
|
||||
return settings.notification_balance_delta_changed
|
||||
return settings.notification_balance_delta_threshold_sats > 0
|
||||
if message_type == NotificationType.server_start_stop:
|
||||
return settings.lnbits_notification_server_start_stop
|
||||
if message_type == NotificationType.server_status:
|
||||
|
||||
@@ -61,6 +61,7 @@ async def pay_invoice(
|
||||
extra: dict | None = None,
|
||||
description: str = "",
|
||||
tag: str = "",
|
||||
labels: list[str] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
if settings.lnbits_only_allow_incoming_payments:
|
||||
@@ -72,6 +73,11 @@ async def pay_invoice(
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
amount_msat = invoice.amount_msat
|
||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
||||
if not wallet.can_send_payments:
|
||||
raise PaymentError(
|
||||
"Wallet does not have permission to pay invoices.",
|
||||
status="failed",
|
||||
)
|
||||
|
||||
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||
@@ -79,16 +85,17 @@ async def pay_invoice(
|
||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||
|
||||
create_payment_model = CreatePayment(
|
||||
wallet_id=wallet_id,
|
||||
wallet_id=wallet.source_wallet_id,
|
||||
bolt11=payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
amount_msat=-amount_msat,
|
||||
expiry=invoice.expiry_date,
|
||||
memo=description or invoice.description or "",
|
||||
extra=extra,
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||
payment = await _pay_invoice(wallet.source_wallet_id, create_payment_model, conn)
|
||||
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
||||
@@ -202,6 +209,7 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
payment_hash=data.payment_hash,
|
||||
labels=data.labels,
|
||||
)
|
||||
|
||||
if data.lnurl_withdraw:
|
||||
@@ -241,6 +249,7 @@ async def create_invoice(
|
||||
webhook: str | None = None,
|
||||
internal: bool | None = False,
|
||||
payment_hash: str | None = None,
|
||||
labels: list[str] | None = None,
|
||||
conn: Connection | None = None,
|
||||
) -> Payment:
|
||||
if not amount > 0:
|
||||
@@ -250,6 +259,12 @@ async def create_invoice(
|
||||
if not user_wallet:
|
||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
||||
|
||||
if not user_wallet.can_receive_payments:
|
||||
raise InvoiceError(
|
||||
"Wallet does not have permission to create invoices.",
|
||||
status="failed",
|
||||
)
|
||||
|
||||
invoice_memo = None if description_hash else memo[:640]
|
||||
|
||||
# use the fake wallet if the invoice is for internal use only
|
||||
@@ -308,7 +323,7 @@ async def create_invoice(
|
||||
invoice = bolt11_decode(invoice_response.payment_request)
|
||||
|
||||
create_payment_model = CreatePayment(
|
||||
wallet_id=wallet_id,
|
||||
wallet_id=user_wallet.source_wallet_id,
|
||||
bolt11=invoice_response.payment_request,
|
||||
payment_hash=invoice.payment_hash,
|
||||
preimage=invoice_response.preimage,
|
||||
@@ -318,6 +333,7 @@ async def create_invoice(
|
||||
extra=extra,
|
||||
webhook=webhook,
|
||||
fee=invoice_response.fee_msat or 0,
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
payment = await create_payment(
|
||||
@@ -456,7 +472,7 @@ async def update_wallet_balance(
|
||||
await create_payment(
|
||||
checking_id=f"internal_{payment_hash}",
|
||||
data=CreatePayment(
|
||||
wallet_id=wallet.id,
|
||||
wallet_id=wallet.source_wallet_id,
|
||||
bolt11=bolt11,
|
||||
payment_hash=payment_hash,
|
||||
amount_msat=amount * 1000,
|
||||
@@ -475,7 +491,7 @@ async def update_wallet_balance(
|
||||
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||
payment = await create_invoice(
|
||||
wallet_id=wallet.id,
|
||||
wallet_id=wallet.source_wallet_id,
|
||||
amount=amount,
|
||||
memo="Admin credit",
|
||||
internal=True,
|
||||
@@ -910,7 +926,7 @@ async def _credit_service_fee_wallet(
|
||||
|
||||
memo = f"""
|
||||
Service fee for payment of {abs(payment.sat)} sats.
|
||||
Wallet: '{wallet.name}' ({wallet.id})."""
|
||||
Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
|
||||
|
||||
create_payment_model = CreatePayment(
|
||||
wallet_id=settings.lnbits_service_fee_wallet,
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
from lnbits.core.crud.users import (
|
||||
get_account,
|
||||
get_account_by_username_or_email,
|
||||
update_account,
|
||||
)
|
||||
from lnbits.core.crud.wallets import (
|
||||
create_wallet,
|
||||
force_delete_wallet,
|
||||
get_standalone_wallet,
|
||||
get_wallet,
|
||||
get_wallets,
|
||||
update_wallet,
|
||||
)
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.users import Account
|
||||
from lnbits.core.models.wallets import (
|
||||
Wallet,
|
||||
WalletSharePermission,
|
||||
WalletShareStatus,
|
||||
WalletType,
|
||||
)
|
||||
from lnbits.db import Connection
|
||||
from lnbits.helpers import sha256s
|
||||
|
||||
|
||||
async def invite_to_wallet(
|
||||
source_wallet: Wallet, data: WalletSharePermission
|
||||
) -> WalletSharePermission:
|
||||
if not source_wallet.is_lightning_wallet:
|
||||
raise ValueError("Only lightning wallets can be shared.")
|
||||
if not data.username:
|
||||
raise ValueError("Username or email missing.")
|
||||
invited_user = await get_account_by_username_or_email(data.username)
|
||||
if not invited_user:
|
||||
raise ValueError("Invited user not found.")
|
||||
|
||||
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||
share = source_wallet.extra.find_share_by_id(request_id)
|
||||
if share:
|
||||
raise ValueError("User already invited to this wallet.")
|
||||
|
||||
invite_request = source_wallet.extra.invite_user_to_shared_wallet(
|
||||
request_id=request_id,
|
||||
request_type=WalletShareStatus.INVITE_SENT,
|
||||
username=data.username,
|
||||
permissions=data.permissions,
|
||||
)
|
||||
await update_wallet(source_wallet)
|
||||
|
||||
wallet_owner = await get_account(source_wallet.user)
|
||||
if not wallet_owner:
|
||||
raise ValueError("Cannot find wallet owner.")
|
||||
invited_user.extra.add_wallet_invite_request(
|
||||
request_id=request_id,
|
||||
from_user_name=wallet_owner.username or wallet_owner.email,
|
||||
to_wallet_id=source_wallet.id,
|
||||
to_wallet_name=source_wallet.name,
|
||||
)
|
||||
await update_account(invited_user)
|
||||
|
||||
return invite_request
|
||||
|
||||
|
||||
async def reject_wallet_invitation(invited_user_id: str, share_request_id: str):
|
||||
invited_user = await get_account(invited_user_id)
|
||||
if not invited_user:
|
||||
raise ValueError("Invited user not found.")
|
||||
|
||||
existing_request = invited_user.extra.find_wallet_invite_request(share_request_id)
|
||||
if not existing_request:
|
||||
raise ValueError("Invitation not found.")
|
||||
|
||||
invited_user.extra.remove_wallet_invite_request(share_request_id)
|
||||
await update_account(invited_user)
|
||||
|
||||
|
||||
async def update_wallet_share_permissions(
|
||||
source_wallet: Wallet, data: WalletSharePermission
|
||||
) -> WalletSharePermission:
|
||||
if not source_wallet.is_lightning_wallet:
|
||||
raise ValueError("Only lightning wallets can be shared.")
|
||||
if not data.shared_with_wallet_id:
|
||||
raise ValueError("Wallet ID missing.")
|
||||
|
||||
share = source_wallet.extra.find_share_for_wallet(data.shared_with_wallet_id)
|
||||
if not share:
|
||||
raise ValueError("Share not found")
|
||||
|
||||
if not share.shared_with_wallet_id:
|
||||
raise ValueError("Share does not have a mirror wallet ID.")
|
||||
|
||||
mirror_wallet = await get_wallet(share.shared_with_wallet_id)
|
||||
if not mirror_wallet:
|
||||
raise ValueError("Target wallet not found")
|
||||
if not mirror_wallet.is_lightning_shared_wallet:
|
||||
raise ValueError("Target wallet is not a shared wallet.")
|
||||
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||
raise ValueError("Not the owner of the shared wallet.")
|
||||
|
||||
share.approve(permissions=data.permissions)
|
||||
await update_wallet(source_wallet)
|
||||
return share
|
||||
|
||||
|
||||
async def delete_wallet_share(source_wallet: Wallet, request_id: str) -> SimpleStatus:
|
||||
if not source_wallet.is_lightning_wallet:
|
||||
raise ValueError("Source wallet is not a lightning wallet.")
|
||||
|
||||
share = source_wallet.extra.find_share_by_id(request_id)
|
||||
if not share:
|
||||
raise ValueError("Wallet share not found.")
|
||||
source_wallet.extra.remove_share_by_id(request_id)
|
||||
|
||||
invited_user = await get_account_by_username_or_email(share.username)
|
||||
if not invited_user:
|
||||
await update_wallet(source_wallet)
|
||||
return SimpleStatus(
|
||||
success=True, message="Permission removed. Invited user not found."
|
||||
)
|
||||
if invited_user.extra.find_wallet_invite_request(request_id):
|
||||
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||
await update_account(invited_user)
|
||||
|
||||
mirror_wallets = await get_wallets(
|
||||
invited_user.id, wallet_type=WalletType.LIGHTNING_SHARED
|
||||
)
|
||||
mirror_wallet = next(
|
||||
(w for w in mirror_wallets if w.shared_wallet_id == source_wallet.id), None
|
||||
)
|
||||
|
||||
if not mirror_wallet:
|
||||
await update_wallet(source_wallet)
|
||||
return SimpleStatus(
|
||||
success=True, message="Permission removed. Target wallet not found."
|
||||
)
|
||||
|
||||
if not mirror_wallet.is_lightning_shared_wallet:
|
||||
raise ValueError("Target wallet is not a shared lightning wallet.")
|
||||
|
||||
if mirror_wallet.shared_wallet_id != source_wallet.id:
|
||||
raise ValueError("Not the owner of the shared wallet.")
|
||||
|
||||
await force_delete_wallet(mirror_wallet.id)
|
||||
|
||||
await update_wallet(source_wallet)
|
||||
return SimpleStatus(success=True, message="Permission removed.")
|
||||
|
||||
|
||||
async def create_lightning_shared_wallet(
|
||||
user_id: str,
|
||||
source_wallet_id: str,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
source_wallet = await get_standalone_wallet(source_wallet_id, conn=conn)
|
||||
if not source_wallet:
|
||||
raise ValueError("Shared wallet does not exist.")
|
||||
|
||||
if not source_wallet.is_lightning_wallet:
|
||||
raise ValueError("Shared wallet is not a lightning wallet.")
|
||||
|
||||
if source_wallet.user == user_id:
|
||||
raise ValueError("Cannot mirror your own wallet.")
|
||||
|
||||
invited_user = await get_account(user_id, conn=conn)
|
||||
if not invited_user:
|
||||
raise ValueError("Cannot find invited user.")
|
||||
|
||||
return await _accept_invitation_to_shared_wallet(
|
||||
invited_user, source_wallet, conn=conn
|
||||
)
|
||||
|
||||
|
||||
async def _accept_invitation_to_shared_wallet(
|
||||
invited_user: Account,
|
||||
source_wallet: Wallet,
|
||||
conn: Connection | None = None,
|
||||
) -> Wallet:
|
||||
request_id = sha256s(invited_user.id + source_wallet.id)
|
||||
existing_request = source_wallet.extra.find_share_by_id(request_id)
|
||||
if not existing_request:
|
||||
raise ValueError("No invitation found for this invited user.")
|
||||
if existing_request.status == WalletShareStatus.APPROVED:
|
||||
raise ValueError("This wallet is already shared with you.")
|
||||
if existing_request.status != WalletShareStatus.INVITE_SENT:
|
||||
raise ValueError("Unknown request type.")
|
||||
|
||||
invited_user.extra.remove_wallet_invite_request(request_id)
|
||||
await update_account(invited_user)
|
||||
|
||||
# todo: double check if user already has a mirror wallet for this source wallet
|
||||
|
||||
mirror_wallet = await create_wallet(
|
||||
user_id=invited_user.id,
|
||||
wallet_name=source_wallet.name,
|
||||
wallet_type=WalletType.LIGHTNING_SHARED,
|
||||
shared_wallet_id=source_wallet.id,
|
||||
conn=conn,
|
||||
)
|
||||
existing_request.approve(shared_with_wallet_id=mirror_wallet.id)
|
||||
await update_wallet(source_wallet, conn=conn)
|
||||
mirror_wallet.mirror_shared_wallet(source_wallet)
|
||||
return mirror_wallet
|
||||
@@ -35,7 +35,7 @@ async def run_by_the_minute_tasks() -> None:
|
||||
while settings.lnbits_running:
|
||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||
|
||||
if settings.notification_balance_delta_changed:
|
||||
if settings.notification_balance_delta_threshold_sats > 0:
|
||||
try:
|
||||
# runs by default every minute, the delta should not change that often
|
||||
await check_balance_delta_changed()
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
<q-tab-panel name="exchange_providers">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('exchange_providers')"></span>
|
||||
</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="q-pa-sm">
|
||||
<canvas
|
||||
style="
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 350px;
|
||||
max-height: 50vh;
|
||||
"
|
||||
ref="exchangeRatesChart"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_refresh_interval_seconds"
|
||||
type="number"
|
||||
label="Refresh Interval (seconds)"
|
||||
hint="How often should the exchange rates be fetched. Set to zero to disable."
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_history_size"
|
||||
type="number"
|
||||
label="History Size"
|
||||
hint="How many data points should be kept in memory."
|
||||
>
|
||||
</q-input>
|
||||
<ul>
|
||||
<li>
|
||||
<code>Refresh Interval</code> and <code> History Size </code>are for
|
||||
historical purposes only.
|
||||
</li>
|
||||
<li>These two settings do not affect the live price computation.</li>
|
||||
<li>
|
||||
Chart currency:
|
||||
<strong
|
||||
><span
|
||||
v-text="formData.lnbits_default_accounting_currency || 'USD'"
|
||||
></span
|
||||
></strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
@click="addExchangeProvider()"
|
||||
label="Add Exchange Provider"
|
||||
color="primary"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
@click="getDefaultSetting('lnbits_exchange_rate_providers')"
|
||||
flat
|
||||
:label="$t('reset_defaults')"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
row-key="name"
|
||||
:rows="formData.lnbits_exchange_rate_providers"
|
||||
:columns="exchangesTable.columns"
|
||||
v-model:pagination="exchangesTable.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td>
|
||||
<q-btn
|
||||
@click="removeExchangeProvider(props.row)"
|
||||
round
|
||||
icon="delete"
|
||||
size="sm"
|
||||
color="negative"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.name"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input>
|
||||
</q-td>
|
||||
<q-td full-width>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.api_url"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input
|
||||
></q-td>
|
||||
<q-td>
|
||||
<q-input
|
||||
dense
|
||||
filled
|
||||
v-model="props.row.path"
|
||||
@update:model-value="touchSettings()"
|
||||
type="text"
|
||||
>
|
||||
</q-input>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="props.row.exclude_to"
|
||||
@update:model-value="touchSettings()"
|
||||
multiple
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
@click="showTickerConversionDialog(props.row)"
|
||||
round
|
||||
icon="add"
|
||||
size="sm"
|
||||
color="gray"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
</q-btn>
|
||||
<q-chip
|
||||
v-for="ticker, index in props.row.ticker_conversion"
|
||||
:key="ticker"
|
||||
removable
|
||||
dense
|
||||
filled
|
||||
@remove="removeExchangeTickerConversion(props.row, ticker)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
:label="ticker"
|
||||
class="ellipsis"
|
||||
>
|
||||
</q-chip>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
<ul>
|
||||
<li>
|
||||
<code>API URL</code> and <code>JSON Path</code> fields can use the
|
||||
<code>{to}</code> and <code>{TO}</code> placeholders for the code of the
|
||||
currency
|
||||
</li>
|
||||
<li>
|
||||
<code>{TO}</code> is the uppercase code and <code>{to}</code> is the
|
||||
lowercase code
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<q-separator class="q-ma-md"></q-separator>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_exchange_rate_cache_seconds"
|
||||
type="number"
|
||||
label="Exchange rate cache (seconds)"
|
||||
hint="For how many seconds should the exchange rate be cached."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-8 col-sm-12"></div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
@@ -1,67 +0,0 @@
|
||||
<q-tab-panel name="library">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('image_library')"></span>
|
||||
</h6>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Add Image"
|
||||
@click="$refs.imageInput.click()"
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref="imageInput"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
style="display: none"
|
||||
@change="onImageInput"
|
||||
/>
|
||||
</q-card-section>
|
||||
<div class="row q-col-gutter-sm q-pa-sm">
|
||||
<div
|
||||
v-for="image in library_images"
|
||||
:key="image.filename"
|
||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
<q-card class="q-mb-sm">
|
||||
<q-img :src="image.url" style="height: 150px" />
|
||||
|
||||
<q-card-section
|
||||
class="q-pt-md q-pb-md row items-center justify-between"
|
||||
>
|
||||
<small
|
||||
><div
|
||||
class="text-caption ellipsis"
|
||||
style="max-width: 100px"
|
||||
:title="image.filename"
|
||||
v-text="image.filename"
|
||||
></div
|
||||
></small>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="content_copy"
|
||||
@click="copyText(image.url)"
|
||||
:title="$t('copy')"
|
||||
><q-tooltip>Copy image link</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
size="sm"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
@click="deleteImage(image.filename)"
|
||||
:title="$t('delete')"
|
||||
><q-tooltip>Delete image</q-tooltip></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="library_images.length === 0" class="q-pa-xl">
|
||||
<div class="text-subtitle2 text-grey">No images uploaded yet.</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
@@ -1,251 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %} {% from "macros.jinja"
|
||||
import window_vars with context %} {% block scripts %} {{ window_vars(user) }}
|
||||
{% endblock %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm">
|
||||
<div class="row items-center justify-between q-gutter-xs">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
:label="$t('save')"
|
||||
color="primary"
|
||||
@click="updateSettings"
|
||||
:disabled="!checkChanges"
|
||||
>
|
||||
<q-tooltip v-if="checkChanges">
|
||||
<span v-text="$t('save_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="checkChanges"
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
style="padding: 6px; border-radius: 6px"
|
||||
/>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="isSuperUser"
|
||||
:label="$t('restart')"
|
||||
color="primary"
|
||||
@click="restartServer"
|
||||
class="q-ml-md"
|
||||
>
|
||||
<q-tooltip v-if="needsRestart">
|
||||
<span v-text="$t('restart_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
|
||||
<q-badge
|
||||
v-if="needsRestart"
|
||||
color="red"
|
||||
rounded
|
||||
floating
|
||||
style="padding: 6px; border-radius: 6px"
|
||||
/>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
:label="$t('download_backup')"
|
||||
flat
|
||||
@click="downloadBackup"
|
||||
></q-btn>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
v-if="isSuperUser"
|
||||
:label="$t('reset_defaults')"
|
||||
color="primary"
|
||||
@click="deleteSettings"
|
||||
class="float-right"
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('reset_defaults_tooltip')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-splitter>
|
||||
<template v-slot:before>
|
||||
<q-tabs
|
||||
@update:model-value="showExchangeProvidersTab"
|
||||
v-model="tab"
|
||||
vertical
|
||||
active-color="primary"
|
||||
>
|
||||
<q-tab
|
||||
name="funding"
|
||||
icon="account_balance_wallet"
|
||||
:label="$q.screen.gt.sm ? $t('funding') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('funding')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="security"
|
||||
icon="security"
|
||||
:label="$q.screen.gt.sm ? $t('security') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('security')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="server"
|
||||
icon="price_change"
|
||||
:label="$q.screen.gt.sm ? $t('payments') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('payments')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="exchange_providers"
|
||||
icon="show_chart"
|
||||
:label="$q.screen.gt.sm ? $t('exchanges') : null"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('exchanges')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="fiat_providers"
|
||||
icon="credit_score"
|
||||
:label="$q.screen.gt.sm ? $t('fiat_providers') : null"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('fiat_providers')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="users"
|
||||
icon="group"
|
||||
:label="$q.screen.gt.sm ? $t('users') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('users')"></span></q-tooltip
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="extensions"
|
||||
icon="extension"
|
||||
:label="$q.screen.gt.sm ? $t('extensions') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('extensions')"></span></q-tooltip
|
||||
></q-tab>
|
||||
|
||||
<q-tab
|
||||
name="notifications"
|
||||
icon="notifications"
|
||||
:label="$q.screen.gt.sm ? $t('notifications') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('notifications')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="audit"
|
||||
icon="playlist_add_check_circle"
|
||||
:label="$q.screen.gt.sm ? $t('audit') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('audit')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="library"
|
||||
icon="image"
|
||||
:label="$q.screen.gt.sm ? $t('library') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('library')"></span></q-tooltip
|
||||
></q-tab>
|
||||
<q-tab
|
||||
style="word-break: break-all"
|
||||
name="site_customisation"
|
||||
icon="language"
|
||||
:label="$q.screen.gt.sm ? $t('site_customisation') : null"
|
||||
@update="val => tab = val.name"
|
||||
><q-tooltip v-if="!$q.screen.gt.sm"
|
||||
><span v-text="$t('site_customisation')"></span></q-tooltip
|
||||
></q-tab>
|
||||
</q-tabs>
|
||||
</template>
|
||||
|
||||
<template v-slot:after>
|
||||
<q-form name="settings_form" id="settings_form">
|
||||
<q-scroll-area style="height: 100vh">
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
animated
|
||||
swipeable
|
||||
vertical
|
||||
scroll
|
||||
transition-prev="jump-up"
|
||||
transition-next="jump-up"
|
||||
>
|
||||
{% include "admin/_tab_funding.html" %} {% include
|
||||
"admin/_tab_users.html" %} {% include "admin/_tab_server.html"
|
||||
%} {% include "admin/_tab_exchange_providers.html" %}{% include
|
||||
"admin/_tab_fiat_providers.html" %} {% include
|
||||
"admin/_tab_extensions.html" %} {% include
|
||||
"admin/_tab_notifications.html" %} {% include
|
||||
"admin/_tab_security.html" %} {% include "admin/_tab_theme.html"
|
||||
%}{% include "admin/_tab_audit.html"%}{% include
|
||||
"admin/_tab_library.html"%}
|
||||
</q-tab-panels>
|
||||
</q-scroll-area>
|
||||
</q-form>
|
||||
</template>
|
||||
</q-splitter>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="exchangeData.showTickerConversion" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<div class="q-mb-md">
|
||||
<strong v-text="$t('create_ticker_converter')"></strong>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 q-mb-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="exchangeData.convertFromTicker"
|
||||
label="From Currency"
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="exchangeData.convertToTicker"
|
||||
dense
|
||||
filled
|
||||
label="New Ticker"
|
||||
hint="This ticker will be used for the exchange API calls."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="addExchangeTickerConversion()"
|
||||
label="Add Ticker Conversion"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
{% endblock %}
|
||||
@@ -1,201 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm q-pl-lg">
|
||||
<div class="row items-center justify-between q-gutter-xs">
|
||||
<div class="col">
|
||||
<!-- Optional: Add content here if needed -->
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
to="/admin#audit"
|
||||
>
|
||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center q-mb-lg">
|
||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('components')"></strong>
|
||||
<div style="width: 250px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="componentUseChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('long_running_endpoints')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="longDurationChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('http_request_methods')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="requestMethodChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-sm-12 text-center">
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('http_response_codes')"></strong>
|
||||
<div style="width: 250px; height: 250px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="responseCodeChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col">
|
||||
<q-card class="q-pa-md">
|
||||
<q-table
|
||||
row-key="id"
|
||||
:rows="auditEntries"
|
||||
:columns="auditTable.columns"
|
||||
v-model:pagination="auditTable.pagination"
|
||||
:filter="auditTable.search"
|
||||
:loading="auditTable.loading"
|
||||
@request="fetchAudit"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-input
|
||||
v-if="['ip_address', 'user_id', 'path',].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
@keydown.enter="searchAuditBy()"
|
||||
@update:model-value="searchAuditBy()"
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
clearable
|
||||
:label="col.label"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
name="search"
|
||||
@click="searchAuditBy()"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
v-else-if="['component', 'response_code','request_method'].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
:options="searchOptions[col.name]"
|
||||
@update:model-value="searchAuditBy()"
|
||||
:label="col.label"
|
||||
clearable
|
||||
style="width: 100px"
|
||||
></q-select>
|
||||
|
||||
<span v-else v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr auto-width :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<div v-if="col.name == 'created_at'">
|
||||
<q-btn
|
||||
icon="description"
|
||||
:disable="!props.row.request_details"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="showDetailsDialog(props.row)"
|
||||
>
|
||||
<q-tooltip
|
||||
><span v-text="$t('request_details')"></span
|
||||
></q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<span v-text="formatDate(props.row.created_at)"></span>
|
||||
<q-tooltip v-if="props.row.delete_at">
|
||||
<span
|
||||
v-text="'Will be deleted at: ' + formatDate(props.row.delete_at)"
|
||||
></span>
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="['user_id', 'request_details'].includes(col.name)"
|
||||
>
|
||||
<q-btn
|
||||
v-if="props.row[col.name]"
|
||||
icon="content_copy"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyText(props.row[col.name])"
|
||||
>
|
||||
<q-tooltip>Copy</q-tooltip>
|
||||
</q-btn>
|
||||
<span v-text="shortify(props.row[col.name])"> </span>
|
||||
<q-tooltip>
|
||||
<span v-text="props.row[col.name]"></span>
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
v-text="props.row[col.name]"
|
||||
@click="searchAuditBy(col.name, props.row[col.name])"
|
||||
class="cursor-pointer"
|
||||
></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="auditDetailsDialog.show" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<strong v-text="$t('http_request_details')"></strong>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="auditDetailsDialog.data"
|
||||
type="textarea"
|
||||
rows="25"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
@click="copyText(auditDetailsDialog.data)"
|
||||
icon="copy_content"
|
||||
color="grey"
|
||||
flat
|
||||
v-text="$t('copy')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,305 +0,0 @@
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="vpn_key"
|
||||
:label="$t('api_keys_api_docs')"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card-section>
|
||||
<q-list>
|
||||
<q-item dense class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>Node URL: </strong><em v-text="origin"></em>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>Wallet ID: </strong><em v-text="wallet.id"></em>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
class="cursor-pointer"
|
||||
@click="copyText(wallet.id)"
|
||||
></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>Admin key: </strong
|
||||
><em
|
||||
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
|
||||
></em>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-icon
|
||||
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="adminkeyHidden = !adminkeyHidden"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
class="cursor-pointer q-ml-sm"
|
||||
@click="copyText(wallet.adminkey)"
|
||||
></q-icon>
|
||||
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
||||
<q-popup-proxy>
|
||||
<div class="q-pa-md">
|
||||
<lnbits-qrcode
|
||||
:value="wallet.adminkey"
|
||||
:show-buttons="false"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item dense class="q-pa-none">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong>Invoice/read key: </strong
|
||||
><em v-text="inkeyHidden ? '****************' : wallet.inkey"></em>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div>
|
||||
<q-icon
|
||||
:name="inkeyHidden ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="inkeyHidden = !inkeyHidden"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
class="cursor-pointer q-ml-sm"
|
||||
@click="copyText(wallet.inkey)"
|
||||
></q-icon>
|
||||
<q-icon name="qr_code" class="cursor-pointer q-ml-sm">
|
||||
<q-popup-proxy>
|
||||
<div class="q-pa-md">
|
||||
<lnbits-qrcode
|
||||
:value="wallet.inkey"
|
||||
:show-buttons="false"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get wallet details"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">GET</span> /api/v1/wallet</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"id": <string>, "name": <string>, "balance":
|
||||
<int>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl <span v-text="baseUrl"></span>api/v1/wallet -H "X-Api-Key:
|
||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i
|
||||
>"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create an invoice (incoming)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-light-green">POST</span> /api/v1/payments</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"out": false, "amount": <int>, "memo": <string>,
|
||||
"expiry": <int>, "unit": <string>, "webhook":
|
||||
<url:string>, "internal": <bool>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"payment_hash": <string>, "payment_request":
|
||||
<string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||
'{"out": false, "amount": <int>, "memo": <string>}' -H
|
||||
"X-Api-Key:
|
||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||
"Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Pay an invoice (outgoing)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span> /api/v1/payments (reveal
|
||||
admin keys
|
||||
<q-icon
|
||||
:name="adminkeyHidden ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="adminkeyHidden = !adminkeyHidden"
|
||||
></q-icon
|
||||
>)</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="adminkeyHidden ? '****************' : wallet.adminkey"
|
||||
></i
|
||||
>"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"out": true, "bolt11": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>{"payment_hash": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments -d
|
||||
'{"out": true, "bolt11": <string>}' -H "X-Api-Key:
|
||||
<i v-text="adminkeyHidden ? '****************' : wallet.adminkey"></i
|
||||
>" -H "Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Decode an invoice"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-green">POST</span>
|
||||
/api/v1/payments/decode</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code>{"data": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 (application/json)
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST <span v-text="baseUrl"></span>api/v1/payments/decode -d
|
||||
'{"data": <bolt11/lnurl, string>}' -H "Content-type:
|
||||
application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Check an invoice (incoming or outgoing)"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-light-blue">GET</span>
|
||||
/api/v1/payments/<payment_hash></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code
|
||||
>{"X-Api-Key": "<i
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></i
|
||||
>"}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>{"paid": <bool>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET
|
||||
<span v-text="baseUrl"></span>api/v1/payments/<payment_hash> -H
|
||||
"X-Api-Key:
|
||||
<i v-text="inkeyHidden ? '****************' : wallet.inkey"></i>" -H
|
||||
"Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">WS</span>
|
||||
/api/v1/ws/<invoice_key></code
|
||||
>
|
||||
<h5
|
||||
class="text-caption q-mt-sm q-mb-none"
|
||||
v-text="$t('websocket_example')"
|
||||
></h5>
|
||||
<code
|
||||
>wscat -c <span v-text="websocketUrl"></span>/<span
|
||||
v-text="inkeyHidden ? '****************' : wallet.inkey"
|
||||
></span
|
||||
></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)/payments
|
||||
</h5>
|
||||
<code>{"balance": <int>, "payment": <object>}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section>
|
||||
<p v-text="$t('reset_wallet_keys_desc')"></p>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="red-10"
|
||||
@click="resetKeys()"
|
||||
:label="$t('reset_wallet_keys')"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</q-expansion-item>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,830 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<q-stepper
|
||||
v-model="step"
|
||||
ref="stepper"
|
||||
color="primary"
|
||||
animated
|
||||
header-nav
|
||||
class="q-pt-sm"
|
||||
@update:model-value="onStepChange"
|
||||
>
|
||||
<q-step
|
||||
:name="1"
|
||||
title="Describe"
|
||||
icon="info"
|
||||
:done="step > 1"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<span class="text-h6">
|
||||
Tell us something about your extension:
|
||||
</span>
|
||||
<ul>
|
||||
<li>This is the first step, you can return and change it.</li>
|
||||
<li>
|
||||
The <code>`name`</code> and
|
||||
<code>`sort description`</code> fields are what the users will
|
||||
see when browsing the list of extensions.
|
||||
</li>
|
||||
<li>
|
||||
The <code>`id`</code> field is used internally and in the URL of
|
||||
your extension.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- todo: add icon -->
|
||||
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Upload Existing config"
|
||||
@click="$refs.extensionDataInput.click()"
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref="extensionDataInput"
|
||||
accept="application/json"
|
||||
style="display: none"
|
||||
@change="onJsonDataInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator class="q-mt-sm"></q-separator>
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="extensionData.name"
|
||||
label="Extension Name"
|
||||
hint="The name of your extension"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="extensionData.id"
|
||||
label="Extension Id"
|
||||
hint="Lowercase letters, numbers, and underscores only (snake_case). This will be used in the URL."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="extensionData.short_description"
|
||||
label="Short Description"
|
||||
hint="A short description that is shown in the extension list."
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
filled
|
||||
v-model="extensionData.description"
|
||||
label="Description"
|
||||
hint="A detailed description of your extension."
|
||||
type="textarea"
|
||||
rows="3"
|
||||
maxlength="1000"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="2"
|
||||
title="Settings"
|
||||
icon="settings"
|
||||
:done="step > 2"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<iframe
|
||||
ref="iframeStep2"
|
||||
class="full-width"
|
||||
height="400px"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-btn
|
||||
@click="previewExtension('settings')"
|
||||
color="primary"
|
||||
outline
|
||||
label="Refresh Preview"
|
||||
class="full-width q-mb-md"
|
||||
></q-btn>
|
||||
<q-toggle
|
||||
v-model="extensionData.settings_data.enabled"
|
||||
label="Generate Settings Fields"
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<ul>
|
||||
<li>Define what settings your extension will have.</li>
|
||||
<li>
|
||||
You can choose if each user has its own settings or if the
|
||||
settings are global (set by the admin).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator
|
||||
v-if="extensionData.settings_data.enabled"
|
||||
class="q-mt-sm"
|
||||
></q-separator>
|
||||
<div v-if="extensionData.settings_data.enabled" class="row q-mt-lg">
|
||||
<div class="col-md-2 col-sm-12">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.settings_data.type"
|
||||
:options="settingsTypes"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-md-10 col-sm-12 q-pt-sm">
|
||||
<q-badge
|
||||
v-if="extensionData.settings_data.type === 'user'"
|
||||
outline
|
||||
class="text-caption q-ml-md"
|
||||
>Each user can set its own settings for this extension.</q-badge
|
||||
>
|
||||
<q-badge v-else outline class="text-caption q-ml-md"
|
||||
>Settings are set by the admin and apply to all users of the
|
||||
extension</q-badge
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="extensionData.settings_data.enabled" class="row q-mt-lg">
|
||||
<div class="col-12">
|
||||
<lnbits-data-fields
|
||||
:fields="extensionData.settings_data.fields"
|
||||
:hide-advanced="true"
|
||||
></lnbits-data-fields>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="3"
|
||||
:done="step > 3"
|
||||
title="Owner Data"
|
||||
icon="list"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<iframe
|
||||
ref="iframeStep3"
|
||||
class="full-width"
|
||||
height="400px"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-btn
|
||||
@click="previewExtension('owner_data')"
|
||||
color="primary"
|
||||
outline
|
||||
label="Refresh Preview"
|
||||
class="full-width q-mb-md"
|
||||
></q-btn>
|
||||
<q-input
|
||||
v-model="extensionData.owner_data.name"
|
||||
filled
|
||||
label="Owner Table Name"
|
||||
hint="CamelCase name for the owner data table (e.g. Campaign, PoS, etc.)"
|
||||
class="q-mb-xl"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
The owner of the extension manages this data. It can add, remove
|
||||
and update instances of it.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Some fileds are present by default, like
|
||||
<code>created_at</code>, <code>updated_at</code> and
|
||||
<code>extra</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col-12">
|
||||
<lnbits-data-fields
|
||||
:fields="extensionData.owner_data.fields"
|
||||
></lnbits-data-fields>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="4"
|
||||
:done="step > 4"
|
||||
title="Client Data"
|
||||
icon="blur_linear"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<iframe
|
||||
ref="iframeStep4"
|
||||
class="full-width"
|
||||
height="400px"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-btn
|
||||
@click="previewExtension('client_data')"
|
||||
color="primary"
|
||||
outline
|
||||
label="Refresh Preview"
|
||||
class="full-width q-mb-md"
|
||||
></q-btn>
|
||||
<!-- <q-toggle
|
||||
v-model="extensionData.client_data.enabled"
|
||||
label="Generate Client Table"
|
||||
disable
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
|
||||
<br /> -->
|
||||
<q-input
|
||||
v-if="extensionData.client_data.enabled"
|
||||
v-model="extensionData.client_data.name"
|
||||
filled
|
||||
label="Client Table Name"
|
||||
hint="CamelCase name for the client data table (e.g. Donation, Payment, etc.)"
|
||||
class="q-mb-xl"
|
||||
>
|
||||
</q-input>
|
||||
<ul>
|
||||
<li>
|
||||
This data is created by users of the extension. Usually when
|
||||
they submit a form or make a payment.
|
||||
</li>
|
||||
<li>
|
||||
The owner of the extension can view this data, but should not
|
||||
modify it.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator
|
||||
v-if="extensionData.client_data.enabled"
|
||||
class="q-mt-sm"
|
||||
></q-separator>
|
||||
<div v-if="extensionData.client_data.enabled" class="row q-mt-lg">
|
||||
<div class="col-12">
|
||||
<lnbits-data-fields
|
||||
:fields="extensionData.client_data.fields"
|
||||
></lnbits-data-fields>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="5"
|
||||
:done="step > 5"
|
||||
title="Public Pages"
|
||||
icon="link"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<iframe
|
||||
ref="iframeStep5"
|
||||
class="full-width"
|
||||
height="400px"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<q-btn
|
||||
@click="previewExtension('public_page')"
|
||||
color="primary"
|
||||
outline
|
||||
label="Refresh Preview"
|
||||
class="full-width q-mb-md"
|
||||
></q-btn>
|
||||
<q-toggle
|
||||
v-model="extensionData.public_page.has_public_page"
|
||||
label="Generate Public Page"
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
<br />
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Most extensions have a public page that can be shared (this page
|
||||
will still be accessible even if you have restricted access to
|
||||
your LNbits install).
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="extensionData.public_page.has_public_page">
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Public page title</q-item-label>
|
||||
<q-item-label caption
|
||||
>Select the field from the
|
||||
<code v-text="extensionData.owner_data.name"></code>
|
||||
(Owner Data) that will be used as a title for the public
|
||||
page.</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.owner_data_fields.name"
|
||||
:options="[''].concat(extensionData.owner_data.fields.map(f => f.name))"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Public page description</q-item-label>
|
||||
<q-item-label caption
|
||||
>Select the field from the
|
||||
<code v-text="extensionData.owner_data.name"></code>
|
||||
(Owner Data) that will be used as a description for the
|
||||
public page.</q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.owner_data_fields.description"
|
||||
:options="[''].concat(extensionData.owner_data.fields.map(f => f.name))"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Public page inputs</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
Select the fields from the
|
||||
<code v-text="extensionData.client_data.name"></code
|
||||
> (Client Data) that will be shown as inputs in
|
||||
the public page form.
|
||||
</li>
|
||||
<li>You can select multiple fields.</li>
|
||||
<li>
|
||||
A corresponding input field will be created for each
|
||||
selected field.
|
||||
</li>
|
||||
</ul>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
multiple
|
||||
use-chips
|
||||
v-model="extensionData.public_page.client_data_fields.public_inputs"
|
||||
:options="extensionData.client_data.fields.map(f => f.name)"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Generate Action Button</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
If enabled, the public page will have a button to
|
||||
perform an action (e.g. generate a payment request).
|
||||
</li>
|
||||
<li>
|
||||
The action will use the selected input fields from
|
||||
<code v-text="extensionData.client_data.name"></code
|
||||
> (Client Data) as parameters.
|
||||
</li>
|
||||
<li>
|
||||
A corresponding REST API endpoint will be created.
|
||||
</li>
|
||||
</ul></q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
v-model="extensionData.public_page.action_fields.generate_action"
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator
|
||||
v-if="extensionData.public_page.action_fields.generate_action"
|
||||
class="q-mt-sm"
|
||||
></q-separator>
|
||||
|
||||
<div v-if="extensionData.public_page.action_fields.generate_action">
|
||||
<div class="row q-col-gutter-md q-mt-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Generate Payment Logic</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
If enabled, the endpoint will create an invoice from
|
||||
the submitted data and the UI will show the QR code
|
||||
with the invoice.
|
||||
</li>
|
||||
<li>
|
||||
A listener will be created to check for the pay event.
|
||||
</li>
|
||||
<li>You must map the fieds.</li>
|
||||
</ul></q-item-label
|
||||
>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
v-model="extensionData.public_page.action_fields.generate_payment_logic"
|
||||
size="md"
|
||||
color="green"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6"></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="extensionData.public_page.action_fields.generate_action && extensionData.public_page.action_fields.generate_payment_logic"
|
||||
class="row q-col-gutter-md q-mt-md"
|
||||
>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Wallet</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
Select the field from the
|
||||
<code v-text="extensionData.owner_data.name"></code
|
||||
> (Owner Data) that represents the wallet which
|
||||
will generate the invoice and receive the payments.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Only fields with the type <code>Wallet</code> will be
|
||||
shown.
|
||||
</li>
|
||||
</ul>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.action_fields.wallet_id"
|
||||
:options="[''].concat(extensionData.owner_data.fields.filter(f => f.type === 'wallet').map(f => f.name))"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Currency</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
Select the field from the
|
||||
<code v-text="extensionData.owner_data.name"></code
|
||||
> (Owner Data) that represents the currency
|
||||
which will be used to for the amount.
|
||||
</li>
|
||||
<li>
|
||||
Only fields with the type <code>Currency</code> will
|
||||
be shown.
|
||||
</li>
|
||||
<li>Empty if you want to use sats.</li>
|
||||
</ul>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.action_fields.currency"
|
||||
:options="[''].concat(extensionData.owner_data.fields.filter(f => f.type === 'currency').map(f => f.name))"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Amount</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
Select the field from the
|
||||
<code v-text="extensionData.owner_data.name"></code
|
||||
> (Owner Data) or
|
||||
<code v-text="extensionData.client_data.name"></code
|
||||
> (Client Data) that represents the amount (in
|
||||
the selected currency).
|
||||
</li>
|
||||
<li>
|
||||
Only fields with the type <code>Integer</code> and
|
||||
<code>Float</code> will be shown.
|
||||
</li>
|
||||
</ul>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.action_fields.amount_source"
|
||||
:options="amountSource"
|
||||
class="q-mr-sm"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.action_fields.amount"
|
||||
:options="paymentActionAmountFields"
|
||||
></q-select>
|
||||
</div>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label>Paid Flag</q-item-label>
|
||||
<q-item-label caption>
|
||||
<ul class="q-pa-none q-ma-none">
|
||||
<li>
|
||||
Select the field from the
|
||||
<code v-text="extensionData.client_data.name"></code
|
||||
> (Client Data) that will be set to true when
|
||||
the invoice is paid.
|
||||
</li>
|
||||
<li>
|
||||
Only fields with the type <code>Boolean</code> will be
|
||||
shown.
|
||||
</li>
|
||||
</ul>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.public_page.action_fields.paid_flag"
|
||||
:options="[''].concat(extensionData.client_data.fields.filter(f => f.type === 'bool').map(f => f.name))"
|
||||
></q-select>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<q-step
|
||||
:name="6"
|
||||
:done="step > 6"
|
||||
title="Publish"
|
||||
icon="publish"
|
||||
style="min-height: 100px"
|
||||
>
|
||||
<div v-if="g.user.admin" class="row">
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
map-options
|
||||
v-model="extensionData.stub_version"
|
||||
hint="The version of the extension stub. Make sure it is compatible with your LNbits install."
|
||||
:options="extensionStubVersions.map(f => f.version)"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
<q-btn
|
||||
@click="cleanCacheData()"
|
||||
color="grey"
|
||||
outline
|
||||
label="Clean Cache"
|
||||
class="q-ml-md"
|
||||
/>
|
||||
<q-icon
|
||||
name="info"
|
||||
size="md"
|
||||
color="primary"
|
||||
class="cursor-pointer q-ml-xs q-mb-xs"
|
||||
>
|
||||
<q-tooltip>
|
||||
<ul class="q-pl-sm">
|
||||
<li>
|
||||
The extension builder uses caching to speed up the build
|
||||
process.
|
||||
</li>
|
||||
<li>
|
||||
This action clears old data and redownloads the Extension
|
||||
Builder Stub release.
|
||||
</li>
|
||||
</ul>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="g.user.admin" class="row q-mt-md">
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
<div class="row">
|
||||
<q-btn
|
||||
@click="buildExtensionAndDeploy()"
|
||||
color="primary"
|
||||
label="Build and Deploy (Admin Only)"
|
||||
class="col"
|
||||
/>
|
||||
<q-icon
|
||||
name="info"
|
||||
size="md"
|
||||
color="primary"
|
||||
class="cursor-pointer q-ml-sm self-center"
|
||||
>
|
||||
<q-tooltip>
|
||||
<ul class="q-pl-sm">
|
||||
<li>
|
||||
Installs the extension directly to this LNbits instance.
|
||||
</li>
|
||||
<li>
|
||||
The extension will be enabled by default, and available to
|
||||
all users.
|
||||
</li>
|
||||
</ul>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-md-4 col-sm-12 col-xs-12">
|
||||
<div class="row">
|
||||
<q-btn
|
||||
@click="buildExtension()"
|
||||
outline
|
||||
color="gray"
|
||||
label="Download Extension Zip"
|
||||
icon="download"
|
||||
class="col"
|
||||
/>
|
||||
<q-icon
|
||||
name="info"
|
||||
size="md"
|
||||
color="primary"
|
||||
class="cursor-pointer q-ml-sm self-center"
|
||||
>
|
||||
<q-tooltip>
|
||||
Builds the extension and downloads a zip file with the code.
|
||||
You can then install it manually in your LNbits instance.
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-step>
|
||||
|
||||
<template v-slot:navigation>
|
||||
<q-separator></q-separator>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 q-pl-md q-pt-md">
|
||||
<q-btn
|
||||
v-if="step == 1"
|
||||
label="Clear All Data"
|
||||
color="negative"
|
||||
@click="clearAllData"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
flat
|
||||
color="grey-8"
|
||||
class="q-mr-sm"
|
||||
@click="previousStep()"
|
||||
label="Back"
|
||||
icon="chevron_left"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 q-pr-md q-pb-md">
|
||||
<q-stepper-navigation class="float-right">
|
||||
<q-btn
|
||||
v-if="step < 6"
|
||||
@click="nextStep()"
|
||||
color="primary"
|
||||
label="Next"
|
||||
></q-btn>
|
||||
<template v-else>
|
||||
<q-btn
|
||||
@click="exportJsonData()"
|
||||
color="primary"
|
||||
label="Export JSON Data"
|
||||
></q-btn>
|
||||
<q-icon
|
||||
name="info"
|
||||
size="md"
|
||||
color="primary"
|
||||
class="cursor-pointer q-ml-sm self-center"
|
||||
>
|
||||
<q-tooltip>
|
||||
<ul class="q-pl-sm">
|
||||
<li>
|
||||
Exports the config JSON so it can be later imported or
|
||||
shared.
|
||||
</li>
|
||||
<li>
|
||||
This JSON is also added to the zip in a file called
|
||||
`builder.json`.
|
||||
</li>
|
||||
</ul>
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-stepper-navigation>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</q-stepper>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md"></div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,150 +0,0 @@
|
||||
{% extends "public.html" %} {% block page_container %}
|
||||
<q-page-container>
|
||||
<q-page class="q-px-md q-py-lg" :class="{'q-px-lg': $q.screen.gt.xs}">
|
||||
{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center main">
|
||||
<div class="col-10 col-md-8 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section class="grid">
|
||||
<div>
|
||||
<h6 class="q-my-none text-center">
|
||||
<strong v-text="$t('welcome_lnbits')"></strong>
|
||||
<p><span v-text="$t('setup_su_account')"></span></p>
|
||||
</h6>
|
||||
<br />
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
v-model="loginData.username"
|
||||
:label="$t('username')"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="loginData.password"
|
||||
:type="loginData.isPwd ? 'password' : 'text'"
|
||||
autocomplete="off"
|
||||
:label="$t('password')"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="loginData.isPwd = !loginData.isPwd"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
v-model.trim="loginData.passwordRepeat"
|
||||
:type="loginData.isPwdRepeat ? 'password' : 'text'"
|
||||
autocomplete="off"
|
||||
:label="$t('password_repeat')"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password'), (val) => val === loginData.password || $t('invalid_password_repeat')]"
|
||||
><template v-slot:append>
|
||||
<q-icon
|
||||
:name="loginData.isPwdRepeat ? 'visibility_off' : 'visibility'"
|
||||
class="cursor-pointer"
|
||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||
/> </template
|
||||
></q-input>
|
||||
<q-btn
|
||||
@click="setPassword()"
|
||||
unelevated
|
||||
color="primary"
|
||||
:label="$t('login')"
|
||||
:disable="checkPasswordsMatch || !loginData.username || !loginData.password || !loginData.passwordRepeat"
|
||||
></q-btn>
|
||||
</q-form>
|
||||
</div>
|
||||
<div class="hero-wrapper">
|
||||
<div class="hero q-mx-auto"></div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.grid {
|
||||
display: block;
|
||||
}
|
||||
.hero-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.hero {
|
||||
display: block;
|
||||
height: 100%;
|
||||
max-width: 250px;
|
||||
background-image: url("{{ static_url_for('static', 'images/logos/lnbits.svg') }}");
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
.hero-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
loginData: {
|
||||
isPwd: true,
|
||||
isPwdRepeat: true,
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.hasAdminUI = '{{ LNBITS_ADMIN_UI | tojson}}'
|
||||
},
|
||||
computed: {
|
||||
checkPasswordsMatch() {
|
||||
return this.loginData.password !== this.loginData.passwordRepeat
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async setPassword() {
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
'/api/v1/auth/first_install',
|
||||
null,
|
||||
{
|
||||
username: this.loginData.username,
|
||||
password: this.loginData.password,
|
||||
password_repeat: this.loginData.passwordRepeat
|
||||
}
|
||||
)
|
||||
|
||||
window.location.href = '/admin'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %}
|
||||
@@ -1,710 +0,0 @@
|
||||
{% extends "public.html" %} {% block scripts %}
|
||||
<style>
|
||||
:root {
|
||||
--size: 100px;
|
||||
--gap: 25px;
|
||||
}
|
||||
|
||||
.btn-fixed-width {
|
||||
/* width: 45%; */
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.marquee {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
gap: var(--gap);
|
||||
height: max-content;
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
hsl(0 0% 0% / 0),
|
||||
hsl(0 0% 0% / 1) 20%,
|
||||
hsl(0 0% 0% / 1) 80%,
|
||||
hsl(0 0% 0% / 0)
|
||||
);
|
||||
}
|
||||
|
||||
.marquee__group {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: var(--gap);
|
||||
min-width: 100%;
|
||||
animation: scroll-x 60s linear infinite;
|
||||
}
|
||||
|
||||
.marquee:hover .marquee__group {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.marquee__group div {
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
@keyframes scroll-x {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||
{% endblock %} {% block page_container %}
|
||||
<q-page-container>
|
||||
<q-page
|
||||
class="q-px-md q-py-lg content-center"
|
||||
:class="{'q-px-lg': $q.screen.gt.xs}"
|
||||
>
|
||||
{% block page %}
|
||||
<div
|
||||
class="row justify-center items-center"
|
||||
style="min-height: calc(100vh / 1.618)"
|
||||
>
|
||||
<div
|
||||
class="full-width"
|
||||
:style="`max-width: ${'{{ LNBITS_CUSTOM_IMAGE }}' ? '850' : '600'}px`"
|
||||
>
|
||||
<div class="row q-mb-md">
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<h5
|
||||
class="q-my-none"
|
||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||
>
|
||||
{{SITE_TITLE}}
|
||||
</h5>
|
||||
<template v-if="$q.screen.gt.sm">
|
||||
<h6
|
||||
class="q-my-sm"
|
||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||
>
|
||||
{{SITE_TAGLINE}}
|
||||
</h6>
|
||||
<p
|
||||
class="q-my-sm"
|
||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||
>
|
||||
{{SITE_DESCRIPTION}}
|
||||
</p>
|
||||
</template>
|
||||
<!-- <div
|
||||
class="gt-sm"
|
||||
v-html="formatDescription"
|
||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||
></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<q-badge
|
||||
v-if="isAccessTokenExpired"
|
||||
class="q-mx-auto q-mb-md"
|
||||
color="primary"
|
||||
rounded
|
||||
>
|
||||
<div class="text-h6">
|
||||
<span v-text="$t('session_has_expired')"></span>
|
||||
</div>
|
||||
</q-badge>
|
||||
<q-card bordered class="full-width q-py-md">
|
||||
<div class="row">
|
||||
<div
|
||||
class="col-12"
|
||||
:class="{'col-sm-7' : '{{ LNBITS_CUSTOM_IMAGE }}', 'col-lg-6' : '{{ LNBITS_CUSTOM_IMAGE }}'}"
|
||||
>
|
||||
{% if lnurl and LNBITS_NEW_ACCOUNTS_ALLOWED and ("user-id-only"
|
||||
in LNBITS_AUTH_METHODS)%}
|
||||
<div class="full-height content-center">
|
||||
<q-card-section>
|
||||
<div class="text-body1">
|
||||
<span v-text="$t('claim_desc')"></span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="processing"
|
||||
type="a"
|
||||
href="/lnurlwallet?lightning={{ lnurl }}"
|
||||
v-text="$t('press_to_claim')"
|
||||
class="full-width"
|
||||
></q-btn>
|
||||
</q-card-section>
|
||||
</div>
|
||||
{%else%}
|
||||
<username-password
|
||||
v-if="authMethod != 'user-id-only'"
|
||||
:allowed_new_users="allowedRegister"
|
||||
:auth-methods="LNBITS_AUTH_METHODS"
|
||||
:auth-action="authAction"
|
||||
v-model:user-name="username"
|
||||
v-model:password_1="password"
|
||||
v-model:password_2="passwordRepeat"
|
||||
v-model:reset-key="reset_key"
|
||||
@login="login"
|
||||
@register="register"
|
||||
@reset="reset"
|
||||
>
|
||||
<div
|
||||
class="text-center text-grey-6"
|
||||
v-if="authAction !== 'reset'"
|
||||
>
|
||||
<p
|
||||
v-if="authAction === 'login' && allowedRegister"
|
||||
class="q-mb-none"
|
||||
>
|
||||
Not registered?
|
||||
<a
|
||||
href="#"
|
||||
class="text-secondary cursor-pointer"
|
||||
@click.prevent="showRegister('username-password')"
|
||||
>Create an Account</a
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
v-else-if="authAction === 'login' && !allowedRegister"
|
||||
class="q-mb-none"
|
||||
>
|
||||
<span v-text="$t('new_user_not_allowed')"></span>
|
||||
</p>
|
||||
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||
<span v-text="$t('existing_account_question')"></span>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="text-secondary cursor-pointer q-ml-sm"
|
||||
@click.prevent="showLogin('username-password')"
|
||||
v-text="$t('login')"
|
||||
></a>
|
||||
</p>
|
||||
</div>
|
||||
</username-password>
|
||||
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
||||
<user-id-only
|
||||
:allowed_new_users="allowedRegister"
|
||||
v-model:usr="usr"
|
||||
v-model:wallet="walletName"
|
||||
:auth-action="authAction"
|
||||
:auth-method="authMethod"
|
||||
@show-login="showLogin"
|
||||
@show-register="showRegister"
|
||||
@login-usr="loginUsr"
|
||||
@create-wallet="createWallet"
|
||||
>
|
||||
</user-id-only>
|
||||
{%endif%} {% endif %}
|
||||
</div>
|
||||
<div
|
||||
class="col-sm-5 col-lg-6 gt-xs"
|
||||
v-if="'{{ LNBITS_CUSTOM_IMAGE }}'"
|
||||
>
|
||||
<div class="full-height flex flex-center q-pa-lg">
|
||||
<q-img
|
||||
:src="'{{ LNBITS_CUSTOM_IMAGE }}'"
|
||||
:ratio="1"
|
||||
width="250px"
|
||||
></q-img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||
class="full-width q-mb-lg q-mt-sm"
|
||||
>
|
||||
<div class="flex flex-center q-gutter-md q-py-md">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://github.com/lnbits/lnbits"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:label="$t('view_github')"
|
||||
class=""
|
||||
></q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
type="a"
|
||||
href="https://demo.lnbits.com/lnurlp/link/fH59GD"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:label="$t('donate')"
|
||||
class=""
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
{% if AD_SPACE_ENABLED and AD_SPACE %}
|
||||
<div class="q-pt-md full-width">
|
||||
<div class="row justify-center q-mb-xl">
|
||||
<div class="full-width text-center">
|
||||
<span class="text-uppercase text-grey">{{ AD_SPACE_TITLE }}</span>
|
||||
</div>
|
||||
<div class="flex flex-center columm">
|
||||
{% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %}
|
||||
<div class="flex flex-center column q-pr-sm">
|
||||
<a href="{{ AD[0] }}">
|
||||
<img
|
||||
v-if="($q.dark.isActive)"
|
||||
src="{{ AD[1] }}"
|
||||
style="max-width: 420px"
|
||||
class="full-width"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="{{ AD[2] }}"
|
||||
style="max-width: 420px"
|
||||
class="full-width"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div
|
||||
v-if="'{{ LNBITS_DENOMINATION }}' == 'sats' && '{{ SITE_TITLE }}' == 'LNbits' && '{{ LNBITS_SHOW_HOME_PAGE_ELEMENTS }}' == 'True'"
|
||||
class="full-width"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<div class="marquee">
|
||||
<div class="marquee__group">
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/ElementsProject/lightning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/lightningnetwork/lnd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://opennode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://lnpay.co/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/rootzoll/raspiblitz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://start9.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://getumbrel.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://mynodebtc.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/shesek/spark-wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://voltage.cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://breez.technology/sdk/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/breez.png') }}' : '{{ static_url_for('static', 'images/breezl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://blockstream.com/lightning/greenlight/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/greenlight.png') }}' : '{{ static_url_for('static', 'images/greenlightl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://getalby.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://zbd.gg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://phoenix.acinq.co/server"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/phoenixd.png') }}' : '{{ static_url_for('static', 'images/phoenixdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://boltz.exchange/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/boltz.svg') }}' : '{{ static_url_for('static', 'images/boltz.svg') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://www.blink.sv/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blink_logo.png') }}' : '{{ static_url_for('static', 'images/blink_logol.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<!-- # -->
|
||||
</div>
|
||||
<div class="marquee__group">
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/ElementsProject/lightning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/cln.png') }}' : '{{ static_url_for('static', 'images/clnl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/lightningnetwork/lnd"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnd.png') }}' : '{{ static_url_for('static', 'images/lnd.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://opennode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/opennode.png') }}' : '{{ static_url_for('static', 'images/opennodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://lnpay.co/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/lnpay.png') }}' : '{{ static_url_for('static', 'images/lnpayl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/rootzoll/raspiblitz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blitz.png') }}' : '{{ static_url_for('static', 'images/blitzl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://start9.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/start9.png') }}' : '{{ static_url_for('static', 'images/start9l.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://getumbrel.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/umbrel.png') }}' : '{{ static_url_for('static', 'images/umbrell.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://mynodebtc.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/mynode.png') }}' : '{{ static_url_for('static', 'images/mynodel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://github.com/shesek/spark-wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/spark.png') }}' : '{{ static_url_for('static', 'images/sparkl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://voltage.cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/voltage.png') }}' : '{{ static_url_for('static', 'images/voltagel.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://breez.technology/sdk/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/breez.png') }}' : '{{ static_url_for('static', 'images/breezl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://blockstream.com/lightning/greenlight/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/greenlight.png') }}' : '{{ static_url_for('static', 'images/greenlightl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://getalby.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/alby.png') }}' : '{{ static_url_for('static', 'images/albyl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://zbd.gg"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/zbd.png') }}' : '{{ static_url_for('static', 'images/zbdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://phoenix.acinq.co/server"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/phoenixd.png') }}' : '{{ static_url_for('static', 'images/phoenixdl.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://boltz.exchange/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/boltz.svg') }}' : '{{ static_url_for('static', 'images/boltz.svg') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="https://www.blink.sv/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<q-img
|
||||
contain
|
||||
:src="($q.dark.isActive) ? '{{ static_url_for('static', 'images/blink_logo.png') }}' : '{{ static_url_for('static', 'images/blink_logol.png') }}'"
|
||||
></q-img>
|
||||
</a>
|
||||
</div>
|
||||
<!-- # -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm q-pl-lg">
|
||||
<div class="row items-center justify-between q-gutter-xs">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
@click="showAddWalletDialog.show = true"
|
||||
:label="$t('add_wallet')"
|
||||
color="primary"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="float-left">
|
||||
<q-input
|
||||
:label="$t('search_wallets')"
|
||||
dense
|
||||
class="float-right q-pr-xl"
|
||||
v-model="walletsTable.search"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<q-icon name="search"> </q-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
v-if="walletsTable.search !== ''"
|
||||
name="close"
|
||||
@click="walletsTable.search = ''"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<q-table
|
||||
grid
|
||||
grid-header
|
||||
flat
|
||||
bordered
|
||||
:rows="wallets"
|
||||
:columns="walletsTable.columns"
|
||||
v-model:pagination="walletsTable.pagination"
|
||||
:loading="walletsTable.loading"
|
||||
@request="getUserWallets"
|
||||
row-key="id"
|
||||
:filter="filter"
|
||||
hide-header
|
||||
>
|
||||
<template v-slot:item="props">
|
||||
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
|
||||
<q-card
|
||||
class="q-ma-sm cursor-pointer wallet-list-card"
|
||||
style="text-decoration: none"
|
||||
@click="goToWallet(props.row.id)"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row items-center">
|
||||
<q-avatar
|
||||
size="lg"
|
||||
:text-color="$q.dark.isActive ? 'black' : 'grey-3'"
|
||||
:color="props.row.extra.color"
|
||||
:icon="props.row.extra.icon"
|
||||
>
|
||||
</q-avatar>
|
||||
|
||||
<div
|
||||
class="text-h6 q-pl-md ellipsis"
|
||||
class="text-bold"
|
||||
v-text="props.row.name"
|
||||
></div>
|
||||
<q-space> </q-space>
|
||||
<q-btn
|
||||
v-if="props.row.extra.pinned"
|
||||
round
|
||||
color="primary"
|
||||
text-color="black"
|
||||
size="xs"
|
||||
icon="push_pin"
|
||||
class="float-right"
|
||||
style="transform: rotate(30deg)"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<div class="row items-center q-pt-sm">
|
||||
<h6 class="q-my-none ellipsis full-width">
|
||||
<strong
|
||||
v-text="formatBalance(props.row.balance_msat / 1000)"
|
||||
></strong>
|
||||
</h6>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-card-section class="text-left">
|
||||
<small>
|
||||
<strong>
|
||||
<span v-text="$t('currency')"></span>
|
||||
</strong>
|
||||
<span v-text="props.row.currency || 'sat'"></span>
|
||||
</small>
|
||||
<br />
|
||||
<small>
|
||||
<strong>
|
||||
<span v-text="$t('id')"></span>
|
||||
:
|
||||
</strong>
|
||||
<span v-text="props.row.id"></span>
|
||||
</small>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
</q-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-dialog
|
||||
v-model="showAddWalletDialog.show"
|
||||
persistent
|
||||
@hide="showAddWalletDialog = {show: false}"
|
||||
>
|
||||
<q-card style="min-width: 350px">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<span v-text="$t('wallet_name')"></span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-input
|
||||
dense
|
||||
v-model="showAddWalletDialog.name"
|
||||
autofocus
|
||||
@keyup.enter="submitAddWallet()"
|
||||
></q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat :label="$t('cancel')" v-close-popup></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
:label="$t('add_wallet')"
|
||||
v-close-popup
|
||||
@click="submitAddWallet()"
|
||||
></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
|
||||
endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
||||
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
|
||||
%} {% endblock %}
|
||||
@@ -1,379 +0,0 @@
|
||||
<q-tab-panel name="channels">
|
||||
<q-dialog v-model="connectPeerDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
v-model="connectPeerDialog.data.uri"
|
||||
label="Node URI"
|
||||
hint="pubkey@host:port"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('connect')"
|
||||
color="primary"
|
||||
@click="connectPeer"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="setFeeDialog.show">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<label class="text-h6">Set Channel Fee</label>
|
||||
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
|
||||
<q-separator></q-separator>
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="setFeeDialog.data.fee_ppm"
|
||||
label="Fee Rate PPM"
|
||||
></q-input>
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="setFeeDialog.data.fee_base_msat"
|
||||
label="Fee Base msat"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('set')"
|
||||
color="primary"
|
||||
@click="setChannelFee(setFeeDialog.channel_id)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="openChannelDialog.show">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
v-model="openChannelDialog.data.peer_id"
|
||||
label="Peer ID"
|
||||
></q-input>
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="openChannelDialog.data.funding_amount"
|
||||
label="Funding Amount"
|
||||
></q-input>
|
||||
<q-expansion-item icon="warning" label="Advanced">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="column q-gutter-md">
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="openChannelDialog.data.push_amount"
|
||||
label="Push Amount"
|
||||
hint="This gifts sats to the other side!"
|
||||
></q-input>
|
||||
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="openChannelDialog.data.fee_rate"
|
||||
label="Fee Rate"
|
||||
></q-input>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('open')"
|
||||
color="primary"
|
||||
@click="openChannel"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="closeChannelDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<div>
|
||||
<q-checkbox v-model="closeChannelDialog.data.force" label="Force" />
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('close')"
|
||||
color="primary"
|
||||
@click="closeChannel"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="row q-col-gutter-lg">
|
||||
<div class="col-12 col-xl-6">
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="q-gutter-y-sm">
|
||||
<div class="row items-center q-mt-none q-gutter-x-sm no-wrap">
|
||||
<div class="col-grow text-h6 q-my-none col-grow">Channels</div>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="channels.filter"
|
||||
placeholder="Search..."
|
||||
class="col-auto"
|
||||
></q-input>
|
||||
<q-select
|
||||
dense
|
||||
size="sm"
|
||||
style="min-width: 200px"
|
||||
filled
|
||||
multiple
|
||||
clearable
|
||||
v-model="stateFilters"
|
||||
:options="this.states"
|
||||
class="col-auto"
|
||||
></q-select>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
size="md"
|
||||
class="col-auto"
|
||||
@click="showOpenChannelDialog()"
|
||||
>
|
||||
Open channel
|
||||
</q-btn>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-subtitle1 col-grow">Total</div>
|
||||
<lnbits-channel-balance
|
||||
:balance="this.totalBalance"
|
||||
></lnbits-channel-balance>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="this.filteredChannels"
|
||||
:filter="channels.filter"
|
||||
no-data-label="No channels opened"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props" style="height: 0"> </q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<div class="q-pb-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div class="text-subtitle1" v-text="props.row.name"></div>
|
||||
<div class="text-caption" v-if="props.row.peer_id">
|
||||
<span>Peer ID</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.peer_id)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="text-caption col-grow">
|
||||
<span>Fees</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="settings"
|
||||
@click="showSetFeeDialog(props.row.id)"
|
||||
></q-btn>
|
||||
<span v-if="props.row.fee_ppm">
|
||||
<span v-text="props.row.fee_ppm"></span> ppm,
|
||||
<span v-text="props.row.fee_base_msat"></span> msat
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-caption" v-if="props.row.id">
|
||||
<span>Channel ID</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.id)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="text-caption" v-if="props.row.short_id">
|
||||
<span v-text="props.row.short_id"></span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.short_id)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<q-badge
|
||||
rounded
|
||||
:color="states.find(s => s.value == props.row.state)?.color"
|
||||
v-text="states.find(s => s.value == props.row.state)?.label"
|
||||
>
|
||||
</q-badge>
|
||||
<q-btn
|
||||
:disable='props.row.state !== "active"'
|
||||
flat
|
||||
dense
|
||||
size="md"
|
||||
@click="showCloseChannelDialog(props.row)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<lnbits-channel-balance
|
||||
:balance="props.row.balance"
|
||||
:color="props.row.color"
|
||||
></lnbits-channel-balance>
|
||||
</div>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-xl-6">
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="column q-gutter-y-sm">
|
||||
<div
|
||||
class="row items-center q-mt-none justify-between q-gutter-x-md no-wrap"
|
||||
>
|
||||
<div class="col-grow text-h6 q-my-none">Peers</div>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="peers.filter"
|
||||
placeholder="Search..."
|
||||
class="col-auto"
|
||||
></q-input>
|
||||
<q-btn
|
||||
class="col-auto"
|
||||
color="primary"
|
||||
@click="connectPeerDialog.show = true"
|
||||
>
|
||||
Connect Peer
|
||||
</q-btn>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="peers.data"
|
||||
:filter="peers.filter"
|
||||
no-data-label="No transactions made yet"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props" style="height: 0"> </q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<div class="row no-wrap items-center q-gutter-sm">
|
||||
<div class="q-my-sm col-grow">
|
||||
<div
|
||||
class="text-subtitle1 text-bold"
|
||||
v-text="props.row.alias"
|
||||
></div>
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.color}`"
|
||||
class="text-bold"
|
||||
v-text="'#'+props.row.color"
|
||||
>
|
||||
</q-badge>
|
||||
<div
|
||||
class="text-bold"
|
||||
v-text="shortenNodeId(props.row.id)"
|
||||
></div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.id)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="qr_code"
|
||||
@click="showNodeInfoDialog(props.row)"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="showOpenChannelDialog(props.row.id)"
|
||||
>
|
||||
Open channel
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="md"
|
||||
@click="disconnectPeer(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
@@ -1,68 +0,0 @@
|
||||
<q-tab-panel name="dashboard">
|
||||
<q-card-section class="q-pa-none">
|
||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
||||
<div class="row q-col-gutter-lg q-mt-sm">
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
<div class="row q-col-gutter-md q-pb-lg">
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('total_capacity')"
|
||||
:msat="this.channel_stats.total_capacity"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat title="Balance" :msat="this.info.balance_msat" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
title="Fees collected"
|
||||
:msat="this.info.fees?.total_msat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
title="Onchain Balance"
|
||||
:btc="this.info.onchain_balance_sat / 100000000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
title="Onchain Confirmed"
|
||||
:btc="this.info.onchain_confirmed_sat / 100000000"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('avg_channel_size')"
|
||||
:msat="this.channel_stats.avg_size"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('biggest_channel_size')"
|
||||
:msat="this.channel_stats.biggest_size"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-4 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('smallest_channel_size')"
|
||||
:msat="this.channel_stats.smallest_size"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-12 col-md-4 q-gutter-y-md">
|
||||
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
|
||||
<lnbits-channel-stats
|
||||
:stats="this.channel_stats"
|
||||
></lnbits-channel-stats>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
@@ -1,314 +0,0 @@
|
||||
<q-tab-panel name="transactions">
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-dialog v-model="transactionDetailsDialog.show" position="top">
|
||||
<q-card class="my-card">
|
||||
<q-card-section>
|
||||
<div class="text-center q-mb-lg">
|
||||
<div
|
||||
v-if="transactionDetailsDialog.data.isIn && transactionDetailsDialog.data.pending"
|
||||
>
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_received'"
|
||||
:color="'green'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_received')"></span>
|
||||
</div>
|
||||
<div class="row q-my-md">
|
||||
<div class="col-3"><b v-text="$t('payment_hash')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
<span
|
||||
v-text="transactionDetailsDialog.data.payment_hash"
|
||||
></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.payment_hash)"
|
||||
size="1em"
|
||||
color="grey"
|
||||
class="q-mb-xs cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="row"
|
||||
v-if="transactionDetailsDialog.data.preimage && !transactionDetailsDialog.data.pending"
|
||||
>
|
||||
<div class="col-3"><b v-text="$t('payment_proof')"></b>:</div>
|
||||
<div class="col-9 text-wrap mono">
|
||||
<span v-text="transactionDetailsDialog.data.preimage"></span>
|
||||
<q-icon
|
||||
name="content_copy"
|
||||
@click="copyText(transactionDetailsDialog.data.preimage)"
|
||||
size="1em"
|
||||
color="grey"
|
||||
class="q-mb-xs cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="transactionDetailsDialog.data.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a :href="'lightning:' + transactionDetailsDialog.data.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode-vue
|
||||
:value="'lightning:' + transactionDetailsDialog.data.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode-vue>
|
||||
</q-responsive>
|
||||
</a>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(transactionDetailsDialog.data.bolt11)"
|
||||
:label="$t('copy_invoice')"
|
||||
class="q-mt-sm"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<div class="row q-col-gutter-md q-pb-lg"></div>
|
||||
|
||||
<div class="row q-col-gutter-lg">
|
||||
<div class="col-12 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col text-h6 q-my-none">Payments</div>
|
||||
<q-input
|
||||
v-if="payments.length > 10"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.filter"
|
||||
debounce="300"
|
||||
placeholder="Search by tag, memo, amount"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="paymentsTable.data"
|
||||
:columns="paymentsTable.columns"
|
||||
v-model:pagination="paymentsTable.pagination"
|
||||
row-key="payment_hash"
|
||||
no-data-label="No transactions made yet"
|
||||
:filter="paymentsTable.filter"
|
||||
@request="getPayments"
|
||||
>
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="!props.row.pending"
|
||||
size="xs"
|
||||
name="call_made"
|
||||
color="green"
|
||||
@click="showTransactionDetailsDialog(props.row)"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
size="xs"
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
@click="showTransactionDetailsDialog(props.row)"
|
||||
>
|
||||
<q-tooltip>Pending</q-tooltip>
|
||||
</q-icon>
|
||||
<q-dialog
|
||||
v-model="props.row.expand"
|
||||
:props="props"
|
||||
position="top"
|
||||
>
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<div v-if="props.row.isIn && props.row.pending">
|
||||
<q-icon
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
></q-icon>
|
||||
<span v-text="$t('invoice_waiting')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
<div
|
||||
v-if="props.row.bolt11"
|
||||
class="text-center q-mb-lg"
|
||||
>
|
||||
<a :href="'lightning:' + props.row.bolt11">
|
||||
<q-responsive :ratio="1" class="q-mx-xl">
|
||||
<qrcode-vue
|
||||
:value="'lightning:' + props.row.bolt11.toUpperCase()"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
></qrcode-vue>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(props.row.bolt11)"
|
||||
:label="$t('copy_invoice')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('close')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isIn">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_received'"
|
||||
:color="'green'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_received')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isPaid && props.row.isOut">
|
||||
<q-icon
|
||||
size="18px"
|
||||
:name="'call_made'"
|
||||
:color="'pink'"
|
||||
></q-icon>
|
||||
<span v-text="$t('payment_sent')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
<div v-else-if="props.row.isOut && props.row.pending">
|
||||
<q-icon
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
></q-icon>
|
||||
<span v-text="$t('outgoing_payment_pending')"></span>
|
||||
<lnbits-payment-details
|
||||
:payment="props.row"
|
||||
></lnbits-payment-details>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-date="props">
|
||||
<q-td auto-width key="date" :props="props">
|
||||
<lnbits-date :ts="props.row.time"></lnbits-date>
|
||||
</q-td>
|
||||
</template>
|
||||
<template v-slot:body-cell-destination="props">
|
||||
<q-td auto-width key="destination">
|
||||
<div class="row items-center justify-between no-wrap">
|
||||
<q-badge
|
||||
:style="`background-color: #${props.row.destination?.color}`"
|
||||
class="text-bold"
|
||||
v-text="props.row.destination?.alias"
|
||||
></q-badge>
|
||||
<div>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(info.id)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="qr_code"
|
||||
@click="showNodeInfoDialog(props.row.destination)"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-sm">
|
||||
<div class="col text-h6 q-my-none">Invoices</div>
|
||||
<q-input
|
||||
v-if="payments.length > 10"
|
||||
filled
|
||||
dense
|
||||
clearable
|
||||
v-model="paymentsTable.filter"
|
||||
debounce="300"
|
||||
placeholder="Search by tag, memo, amount"
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="invoiceTable.data"
|
||||
:columns="invoiceTable.columns"
|
||||
v-model:pagination="invoiceTable.pagination"
|
||||
no-data-label="No transactions made yet"
|
||||
:filter="invoiceTable.filter"
|
||||
@request="getInvoices"
|
||||
>
|
||||
<template v-slot:body-cell-pending="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="!props.row.pending"
|
||||
size="xs"
|
||||
name="call_received"
|
||||
color="green"
|
||||
@click="showTransactionDetailsDialog(props.row)"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
size="xs"
|
||||
name="settings_ethernet"
|
||||
color="grey"
|
||||
@click="showTransactionDetailsDialog(props.row)"
|
||||
>
|
||||
<q-tooltip>Pending</q-tooltip>
|
||||
</q-icon>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-paid_at="props">
|
||||
<q-td auto-width :props="props">
|
||||
<lnbits-date
|
||||
v-if="props.row.paid_at"
|
||||
:ts="props.row.paid_at"
|
||||
></lnbits-date>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-expiry="props">
|
||||
<q-td auto-width :props="props">
|
||||
<lnbits-date
|
||||
v-if="props.row.expiry"
|
||||
:ts="props.row.expiry"
|
||||
></lnbits-date>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
@@ -1,46 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||
|
||||
<q-dialog v-model="nodeInfoDialog.show" position="top">
|
||||
<lnbits-node-qrcode :info="nodeInfoDialog.data"></lnbits-node-qrcode>
|
||||
</q-dialog>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col q-gutter-y-md">
|
||||
<q-card>
|
||||
<div class="q-pa-md">
|
||||
<div class="q-gutter-y-md">
|
||||
<q-tabs v-model="tab" active-color="primary" align="justify">
|
||||
<q-tab
|
||||
name="dashboard"
|
||||
:label="$t('dashboard')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="channels"
|
||||
:label="$t('channels')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
<q-tab
|
||||
name="transactions"
|
||||
:label="$t('transactions')"
|
||||
@update="val => tab = val.name"
|
||||
></q-tab>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-form name="settings_form" id="settings_form">
|
||||
<q-tab-panels v-model="tab" animated>
|
||||
{% include "node/_tab_dashboard.html" %} {% include
|
||||
"node/_tab_channels.html" %} {% include "node/_tab_transactions.html"
|
||||
%}
|
||||
</q-tab-panels>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,133 +0,0 @@
|
||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
||||
context %} {% block page %}
|
||||
|
||||
<div class="q-ma-lg-xl q-mx-auto q-ma-xl" style="max-width: 1048px">
|
||||
<lnbits-node-info :info="this.info"></lnbits-node-info>
|
||||
|
||||
<div class="row q-col-gutter-lg q-mt-sm">
|
||||
<div class="col-12 col-md-8 q-gutter-y-md">
|
||||
<div class="row q-col-gutter-md q-pb-lg">
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('total_capacity')"
|
||||
:msat="this.channel_stats.total_capacity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat title="Peers" :amount="this.info.num_peers" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('avg_channel_size')"
|
||||
:msat="this.channel_stats.avg_size"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('biggest_channel_size')"
|
||||
:msat="this.channel_stats.biggest_size"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('smallest_channel_size')"
|
||||
:msat="this.channel_stats.smallest_size"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 q-gutter-y-md">
|
||||
<lnbits-stat
|
||||
:title="$t('smallest_channel_size')"
|
||||
:msat="this.channel_stats.smallest_size"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-12 col-md-4 q-gutter-y-md">
|
||||
<lnbits-node-ranks :ranks="this.ranks"></lnbits-node-ranks>
|
||||
<lnbits-channel-stats :stats="this.channel_stats"></lnbits-channel-stats>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="{{ static_url_for('static', 'js/node.js') }}"></script>
|
||||
<script>
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
config: {
|
||||
globalProperties: {
|
||||
LNbits,
|
||||
msg: 'hello'
|
||||
}
|
||||
},
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
wallet: {},
|
||||
tab: 'dashboard',
|
||||
payments: 1000,
|
||||
info: {},
|
||||
channel_stats: {},
|
||||
channels: [],
|
||||
activeBalance: {},
|
||||
ranks: {},
|
||||
|
||||
peers: [],
|
||||
|
||||
connectPeerDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
openChannelDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
closeChannelDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
nodeInfoDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
},
|
||||
|
||||
states: [
|
||||
{label: 'Active', value: 'active', color: 'green'},
|
||||
{label: 'Pending', value: 'pending', color: 'orange'},
|
||||
{label: 'Inactive', value: 'inactive', color: 'grey'},
|
||||
{label: 'Closed', value: 'closed', color: 'red'}
|
||||
]
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.getInfo()
|
||||
this.get1MLStats()
|
||||
},
|
||||
methods: {
|
||||
formatMsat: function (msat) {
|
||||
return LNbits.utils.formatMsat(msat)
|
||||
},
|
||||
api: function (method, url, data) {
|
||||
return LNbits.api.request(method, '/node/public/api/v1' + url, {}, data)
|
||||
},
|
||||
getInfo: function () {
|
||||
this.api('GET', '/info', {}).then(response => {
|
||||
this.info = response.data
|
||||
this.channel_stats = response.data.channel_stats
|
||||
})
|
||||
},
|
||||
get1MLStats: function () {
|
||||
this.api('GET', '/rank', {}).then(response => {
|
||||
this.ranks = response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,432 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm q-pl-lg">
|
||||
<div class="row items-center justify-between q-gutter-xs">
|
||||
<div class="col"></div>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showPaymentStatus"
|
||||
:label="$t('payments_status_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showPaymentTags"
|
||||
:label="$t('payments_tag_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showBalance"
|
||||
:label="$t('payments_balance_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showWalletsSize"
|
||||
:label="$t('payments_wallets_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showBalanceInOut"
|
||||
:label="$t('payments_balance_in_out_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div class="float-left">
|
||||
<q-checkbox
|
||||
dense
|
||||
@click="saveChartsPreferences"
|
||||
v-model="chartData.showPaymentCountInOut"
|
||||
:label="$t('payments_count_in_out_chart')"
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<q-btn icon="event" outline flat>
|
||||
<q-popup-proxy
|
||||
cover
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
>
|
||||
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
label="Search"
|
||||
@click="searchByDate()"
|
||||
color="primary"
|
||||
flat
|
||||
class="float-left"
|
||||
v-close-popup
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
@click="clearDateSeach()"
|
||||
label="Clear"
|
||||
class="float-right"
|
||||
color="grey"
|
||||
flat
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
<q-badge
|
||||
v-if="searchDate?.to || searchDate?.from"
|
||||
class="q-mt-lg q-mr-md"
|
||||
color="primary"
|
||||
rounded
|
||||
floating
|
||||
style="border-radius: 6px"
|
||||
/>
|
||||
</q-btn>
|
||||
<q-separator vertical class="q-ma-sm"></q-separator>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
to="/admin#server"
|
||||
>
|
||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="!showDetails">
|
||||
<div class="row q-col-gutter-md justify-center q-mb-md">
|
||||
<div
|
||||
v-show="chartData.showPaymentStatus"
|
||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('payment_chart_status')"></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsStatusChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-show="chartData.showPaymentStatus"
|
||||
class="col-lg-3 col-md-6 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('payment_chart_tags')"></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsTagsChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-show="chartData.showBalance"
|
||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong
|
||||
v-text="$t('lnbits_balance', {balance: (lnbitsBalance || 0).toLocaleString()})"
|
||||
></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsDailyChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-show="chartData.showWalletsSize"
|
||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('payment_chart_tx_per_wallet')"></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsWalletsChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="chartData.showBalanceInOut"
|
||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('payments_balance_in_out')"></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsBalanceInOutChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
<div
|
||||
v-show="chartData.showPaymentCountInOut"
|
||||
class="col-lg-6 col-md-12 col-sm-12 text-center"
|
||||
>
|
||||
<q-card class="q-pt-sm">
|
||||
<strong v-text="$t('payments_count_in_out')"></strong>
|
||||
<div style="height: 300px" class="q-pa-sm">
|
||||
<canvas v-if="chartsReady" ref="paymentsCountInOutChart"></canvas>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col">
|
||||
<q-card class="q-pa-md">
|
||||
<q-table
|
||||
row-key="payment_hash"
|
||||
:rows="payments"
|
||||
:columns="paymentsTable.columns"
|
||||
v-model:pagination="paymentsTable.pagination"
|
||||
:filter="paymentsTable.search"
|
||||
@request="fetchPayments"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-input
|
||||
v-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
@keydown.enter="searchPaymentsBy()"
|
||||
@update:model-value="searchPaymentsBy()"
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
clearable
|
||||
:label="col.label"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
name="search"
|
||||
@click="searchPaymentsBy()"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-btn
|
||||
v-else-if="['status'].includes(col.name)"
|
||||
flat
|
||||
dense
|
||||
:label="$q.screen.gt.md ? 'Status' : null"
|
||||
icon="filter_alt"
|
||||
color="grey"
|
||||
class="text-capitalize"
|
||||
>
|
||||
<q-menu anchor="top right" self="top start">
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="statusFilters.success"
|
||||
@click="handleFilterChanged"
|
||||
label="Success Payments"
|
||||
></q-checkbox>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="statusFilters.pending"
|
||||
@click="handleFilterChanged"
|
||||
label="Pending Payments"
|
||||
></q-checkbox>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="statusFilters.failed"
|
||||
@click="handleFilterChanged"
|
||||
label="Failed Payments"
|
||||
></q-checkbox>
|
||||
</q-item>
|
||||
<q-separator></q-separator>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="statusFilters.incoming"
|
||||
@click="handleFilterChanged"
|
||||
label="Incoming Payments"
|
||||
></q-checkbox>
|
||||
</q-item>
|
||||
<q-item dense>
|
||||
<q-checkbox
|
||||
v-model="statusFilters.outgoing"
|
||||
@click="handleFilterChanged"
|
||||
label="Outgoing Payments"
|
||||
></q-checkbox>
|
||||
</q-item>
|
||||
</q-menu>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('filter_payments')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
<q-select
|
||||
v-else-if="['tag'].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
:options="searchOptions[col.name]"
|
||||
@update:model-value="searchPaymentsBy()"
|
||||
:label="col.label"
|
||||
clearable
|
||||
style="width: 100px"
|
||||
></q-select>
|
||||
<span v-else v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr auto-width :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<div v-if="col.name == 'status'">
|
||||
<q-tooltip
|
||||
><span v-text="$t('payment_details')"></span
|
||||
></q-tooltip>
|
||||
<q-icon
|
||||
@click="showDetailsToggle(props.row)"
|
||||
v-if="props.row.status === 'success'"
|
||||
size="14px"
|
||||
:name="props.row.outgoing ? 'call_made' : 'call_received'"
|
||||
:color="props.row.outgoing ? 'pink' : 'green'"
|
||||
class="cursor-pointer"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else-if="props.row.status === 'pending'"
|
||||
@click="showDetailsToggle(props.row)"
|
||||
name="downloading"
|
||||
:style="props.row.outgoing ? 'transform: rotate(225deg)' : 'transform: scaleX(-1) rotate(315deg)'"
|
||||
color="grey"
|
||||
class="cursor-pointer"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-else
|
||||
@click="showDetailsToggle(props.row)"
|
||||
name="warning"
|
||||
color="yellow"
|
||||
class="cursor-pointer"
|
||||
></q-icon>
|
||||
</div>
|
||||
<div v-else-if="col.name == 'created_at'">
|
||||
<div>
|
||||
<q-tooltip anchor="top middle">
|
||||
<span v-text="formatDate(props.row.created_at)"></span>
|
||||
</q-tooltip>
|
||||
<span v-text="props.row.timeFrom"> </span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="['wallet_id', 'payment_hash', 'memo'].includes(col.name)"
|
||||
>
|
||||
<q-btn
|
||||
v-if="props.row[col.name]"
|
||||
icon="content_copy"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyText(props.row[col.name])"
|
||||
>
|
||||
<q-tooltip anchor="top middle">Copy</q-tooltip>
|
||||
</q-btn>
|
||||
<span v-text="shortify(props.row[col.name], col.max_length)">
|
||||
</span>
|
||||
<q-tooltip>
|
||||
<span v-text="props.row[col.name]"></span>
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
v-text="props.row[col.name]"
|
||||
class="cursor-pointer"
|
||||
></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="showDetails">
|
||||
<q-card>
|
||||
<q-card-section class="flex">
|
||||
<div>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="arrow_back"
|
||||
class="q-mr-md"
|
||||
@click="showDetailsToggle(null)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="self-center text-h6 text-weight-bolder text-grey-5">
|
||||
<span v-text="$t('payment_details_back')"></span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section class="text-h6">
|
||||
<q-item>
|
||||
<q-item-section avatar class="">
|
||||
<q-icon color="primary" name="receipt" size="44px"></q-icon>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<div class="text-h6">
|
||||
<span v-text="$t('payment_details')"></span>
|
||||
</div>
|
||||
</q-item-label>
|
||||
<q-item-label caption v-text="$t('payment_details_desc')">
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-list separator>
|
||||
<q-item v-for="(value, key) in paymentDetails" :key="key">
|
||||
<q-item-section>
|
||||
<q-item-label v-text="key"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="value"
|
||||
style="word-wrap: break-word"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-btn
|
||||
v-show="value"
|
||||
icon="content_copy"
|
||||
flat
|
||||
class="cursor-pointer q-ml-sm"
|
||||
@click="copyText(value)"
|
||||
>
|
||||
<q-tooltip>Copy</q-tooltip>
|
||||
</q-btn>
|
||||
</q-item-section>
|
||||
<!-- <q-separator></q-separator> -->
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,44 +0,0 @@
|
||||
<q-dialog v-model="createWalletDialog.show" position="top">
|
||||
<q-card class="q-pa-md q-pt-md lnbits__dialog-card">
|
||||
<strong>Create Wallet</strong>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col">
|
||||
<q-input
|
||||
v-model="createWalletDialog.data.name"
|
||||
:label='$t("name_your_wallet")'
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="createWalletDialog.data.currency"
|
||||
:options="{{ currencies | safe }}"
|
||||
></q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-close-popup
|
||||
@click="createWallet()"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Create</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
@@ -1,196 +0,0 @@
|
||||
<div class="row q-mb-lg">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
icon="arrow_back_ios"
|
||||
@click="backToUsersPage()"
|
||||
:label="$t('back')"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-if="activeUser.data.id"
|
||||
@click="updateUser()"
|
||||
color="primary"
|
||||
:label="$t('update_account')"
|
||||
class="q-ml-md"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
@click="createUser()"
|
||||
:label="$t('create_account')"
|
||||
color="primary"
|
||||
class="float-right"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-card v-if="activeUser.show" class="q-pa-md">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<span v-if="activeUser.data.id" v-text="$t('update_account')"></span>
|
||||
<span v-else v-text="$t('create_account')"></span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-if="activeUser.data.id"
|
||||
v-model="activeUser.data.id"
|
||||
:label="$t('user_id')"
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
:type="activeUser.data.showUserId ? 'text': 'password'"
|
||||
class="q-mb-md"
|
||||
><q-btn
|
||||
@click="activeUser.data.showUserId = !activeUser.data.showUserId"
|
||||
dense
|
||||
flat
|
||||
:icon="activeUser.data.showUserId ? 'visibility_off' : 'visibility'"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.username"
|
||||
:label="$t('username')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-toggle
|
||||
size="xs"
|
||||
v-if="!activeUser.data.id"
|
||||
color="secondary"
|
||||
:label="$t('set_password')"
|
||||
v-model="activeUser.setPassword"
|
||||
>
|
||||
<q-tooltip v-text="$t('set_password_tooltip')"></q-tooltip>
|
||||
</q-toggle>
|
||||
|
||||
<q-input
|
||||
v-if="activeUser.setPassword"
|
||||
v-model="activeUser.data.password"
|
||||
:type="activeUser.data.showPassword ? 'text': 'password'"
|
||||
autocomplete="off"
|
||||
:label="$t('password')"
|
||||
filled
|
||||
dense
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
>
|
||||
<q-btn
|
||||
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
||||
dense
|
||||
flat
|
||||
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-if="activeUser.setPassword"
|
||||
v-model="activeUser.data.password_repeat"
|
||||
:type="activeUser.data.showPassword ? 'text': 'password'"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:label="$t('password_repeat')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||
>
|
||||
<q-btn
|
||||
@click="activeUser.data.showPassword = !activeUser.data.showPassword"
|
||||
dense
|
||||
flat
|
||||
:icon="activeUser.data.showPassword ? 'visibility_off' : 'visibility'"
|
||||
color="grey"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="activeUser.data.pubkey"
|
||||
:label="'Nostr '+ $t('pubkey')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
<q-tooltip v-text="$t('nostr_pubkey_tooltip')"></q-tooltip>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.email"
|
||||
:label="$t('email')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-if="activeUser.data.extra">
|
||||
<q-input
|
||||
v-model="activeUser.data.extra.first_name"
|
||||
:label="$t('first_name')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.extra.last_name"
|
||||
:label="$t('last_name')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.extra.provider"
|
||||
:label="$t('auth_provider')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.external_id"
|
||||
:label="$t('external_id')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
v-model="activeUser.data.extra.picture"
|
||||
:label="$t('picture')"
|
||||
filled
|
||||
dense
|
||||
class="q-mb-md"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="activeUser.data.extensions"
|
||||
multiple
|
||||
label="User extensions"
|
||||
:options="g.extensions"
|
||||
></q-select>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="activeUser.data.id">
|
||||
<q-btn
|
||||
@click="resetPassword(activeUser.data.id)"
|
||||
:disable="activeUser.data.is_super_user"
|
||||
:label="$t('reset_password')"
|
||||
icon="refresh"
|
||||
color="primary"
|
||||
>
|
||||
<q-tooltip>Generate and copy password reset url</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
@click="deleteUser(activeUser.data.id)"
|
||||
:disable="activeUser.data.is_super_user"
|
||||
:label="$t('delete')"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
class="float-right"
|
||||
>
|
||||
<q-tooltip>Delete User</q-tooltip></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -1,190 +0,0 @@
|
||||
<div v-if="paymentPage.show">
|
||||
<div class="row q-mb-lg">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
icon="arrow_back_ios"
|
||||
@click="paymentPage.show = false"
|
||||
:label="$t('back')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-card class="q-pa-md">
|
||||
<q-card-section>
|
||||
<payment-list :wallet="paymentsWallet" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div v-else-if="activeWallet.show">
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm">
|
||||
<div class="row">
|
||||
<div class="q-pa-xs">
|
||||
<q-btn
|
||||
icon="arrow_back_ios"
|
||||
@click="backToUsersPage()"
|
||||
:label="$t('back')"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="q-pa-xs">
|
||||
<q-btn
|
||||
@click="createWalletDialog.show = true"
|
||||
:label="$t('create_new_wallet')"
|
||||
color="primary"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="q-pa-xs">
|
||||
<q-btn
|
||||
@click="deleteAllUserWallets(activeWallet.userId)"
|
||||
:label="$t('delete_all_wallets')"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
></q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<q-card class="q-pa-md">
|
||||
<h2 class="text-h6 q-mb-md">Wallets</h2>
|
||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width v-if="g.user.super_user"></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th
|
||||
auto-width
|
||||
v-for="col in props.cols"
|
||||
v-text="col.label"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width v-if="g.user.super_user">
|
||||
<lnbits-update-balance
|
||||
:wallet_id="props.row.id"
|
||||
@credit-value="handleBalanceUpdate"
|
||||
class="q-mr-md"
|
||||
></lnbits-update-balance>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
round
|
||||
icon="menu"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@click="showPayments(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Show Payments</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
round
|
||||
v-if="!props.row.deleted"
|
||||
icon="vpn_key"
|
||||
size="sm"
|
||||
color="primary"
|
||||
class="q-ml-xs"
|
||||
@click="copyText(props.row.adminkey)"
|
||||
>
|
||||
<q-tooltip>Copy Admin Key</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
round
|
||||
v-if="!props.row.deleted"
|
||||
icon="vpn_key"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-ml-xs"
|
||||
@click="copyText(props.row.inkey)"
|
||||
>
|
||||
<q-tooltip>Copy Invoice Key</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
round
|
||||
icon="delete"
|
||||
size="sm"
|
||||
color="negative"
|
||||
class="q-ml-xs"
|
||||
@click="deleteUserWallet(props.row.user, props.row.id, props.row.deleted)"
|
||||
>
|
||||
<q-tooltip>Delete Wallet</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
round
|
||||
v-if="props.row.deleted"
|
||||
icon="toggle_off"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-ml-xs"
|
||||
@click="undeleteUserWallet(props.row.user, props.row.id)"
|
||||
>
|
||||
<q-tooltip>Undelete Wallet</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
icon="link"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyWalletLink(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Copy Wallet Link</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<span
|
||||
v-text="props.row.name"
|
||||
v-if="!props.row.editable"
|
||||
:class="props.row.deleted ? 'text-strike' : 'cursor-pointer'"
|
||||
@click="props.row.editable = true && !props.row.deleted"
|
||||
></span>
|
||||
<q-input
|
||||
v-else
|
||||
@keydown.enter="updateWallet(props.row)"
|
||||
v-model="props.row.name"
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn
|
||||
@click="updateWallet(props.row)"
|
||||
round
|
||||
dense
|
||||
size="xs"
|
||||
flat
|
||||
icon="send"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
icon="content_copy"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyText(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Copy Wallet ID</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<span
|
||||
v-text="props.row.id"
|
||||
:class="props.row.deleted ? 'text-strike' : ''"
|
||||
></span>
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width v-text="props.row.currency"></q-td>
|
||||
<q-td auto-width v-text="formatSat(props.row.balance_msat)"></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
@@ -1,186 +0,0 @@
|
||||
{% if not ajax %} {% extends "base.html" %} {% endif %}
|
||||
<!---->
|
||||
{% from "macros.jinja" import window_vars with context %}
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user) }}{% endblock %} {% block page %}
|
||||
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col">
|
||||
{% include "users/_manageWallet.html" %}
|
||||
<div v-if="activeUser.show" class="row">
|
||||
<div class="col-12 col-md-6">{%include "users/_manageUser.html" %}</div>
|
||||
</div>
|
||||
<div v-else-if="activeWallet.show">
|
||||
{%include "users/_createWalletDialog.html" %}
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-12">
|
||||
<q-card>
|
||||
<div class="q-pa-sm">
|
||||
<div class="row items-center justify-between q-gutter-xs">
|
||||
<div class="col">
|
||||
<q-btn
|
||||
@click="showAccountPage()"
|
||||
:label="$t('create_account')"
|
||||
color="primary"
|
||||
>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
v-if="g.user.admin"
|
||||
flat
|
||||
round
|
||||
icon="settings"
|
||||
to="/admin#users"
|
||||
>
|
||||
<q-tooltip v-text="$t('admin_settings')"></q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-card class="q-pa-md">
|
||||
<q-table
|
||||
row-key="id"
|
||||
:rows="users"
|
||||
:columns="usersTable.columns"
|
||||
v-model:pagination="usersTable.pagination"
|
||||
:no-data-label="$t('no_users')"
|
||||
:filter="usersTable.search"
|
||||
:loading="usersTable.loading"
|
||||
@request="fetchUsers"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-input
|
||||
v-if="['user', 'username', 'email', 'pubkey', 'wallet_id'].includes(col.name)"
|
||||
v-model="searchData[col.name]"
|
||||
@keydown.enter="searchUserBy(col.name)"
|
||||
dense
|
||||
type="text"
|
||||
filled
|
||||
:label="col.label"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-icon
|
||||
name="search"
|
||||
@click="searchUserBy(col.name)"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<span v-else v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr auto-width :props="props">
|
||||
<q-td>
|
||||
<q-btn
|
||||
@click="showAccountPage(props.row.id)"
|
||||
round
|
||||
icon="edit"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
<q-tooltip>
|
||||
<span v-text="$t('update_account')"></span>
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-toggle
|
||||
size="xs"
|
||||
v-if="!props.row.is_super_user"
|
||||
color="secondary"
|
||||
v-model="props.row.is_admin"
|
||||
@update:model-value="toggleAdmin(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Toggle Admin</q-tooltip>
|
||||
</q-toggle>
|
||||
<q-btn
|
||||
round
|
||||
v-if="props.row.is_super_user"
|
||||
icon="verified"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-ml-xs"
|
||||
>
|
||||
<q-tooltip>Super User</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
icon="list"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
:label="props.row.wallet_count"
|
||||
@click="fetchWallets(props.row.id)"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="(users.length == 1) && searchData.wallet_id"
|
||||
round
|
||||
icon="menu"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
class="q-ml-sm"
|
||||
@click="showWalletPayments(searchData.wallet_id)"
|
||||
>
|
||||
<q-tooltip>Show Payments</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
icon="content_copy"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyText(props.row.id)"
|
||||
>
|
||||
<q-tooltip>Copy User ID</q-tooltip>
|
||||
</q-btn>
|
||||
<span v-text="shortify(props.row.id)"></span>
|
||||
</q-td>
|
||||
<q-td v-text="props.row.username"></q-td>
|
||||
|
||||
<q-td v-text="props.row.email"></q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
v-if="props.row.pubkey"
|
||||
icon="content_copy"
|
||||
size="sm"
|
||||
flat
|
||||
class="cursor-pointer q-mr-xs"
|
||||
@click="copyText(props.row.pubkey)"
|
||||
>
|
||||
<q-tooltip>Copy Public Key</q-tooltip>
|
||||
</q-btn>
|
||||
<span v-text="shortify(props.row.pubkey)"></span>
|
||||
</q-td>
|
||||
<q-td v-text="formatSat(props.row.balance_msat)"></q-td>
|
||||
|
||||
<q-td v-text="props.row.transaction_count"></q-td>
|
||||
|
||||
<q-td v-text="formatDate(props.row.last_payment)"></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,19 +1,14 @@
|
||||
import os
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from shutil import make_archive, move
|
||||
from shutil import make_archive
|
||||
from subprocess import Popen
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import IO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import filetype
|
||||
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, Depends, File
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.misc import Image, SimpleStatus
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services import (
|
||||
enqueue_admin_notification,
|
||||
@@ -23,7 +18,6 @@ from lnbits.core.services import (
|
||||
from lnbits.core.services.notifications import send_email_notification
|
||||
from lnbits.core.services.settings import dict_to_settings
|
||||
from lnbits.decorators import check_admin, check_super_user
|
||||
from lnbits.helpers import safe_upload_file_path
|
||||
from lnbits.server import server_restart
|
||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||
from lnbits.tasks import invoice_listeners
|
||||
@@ -172,93 +166,3 @@ async def api_download_backup() -> FileResponse:
|
||||
return FileResponse(
|
||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.post(
|
||||
"/api/v1/images",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def upload_image(
|
||||
file: UploadFile = file_upload,
|
||||
content_length: int = Header(..., le=settings.lnbits_upload_size_bytes),
|
||||
) -> Image:
|
||||
if not file.filename:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail="No filename provided."
|
||||
)
|
||||
|
||||
# validate file types
|
||||
file_info = filetype.guess(file.file)
|
||||
if file_info is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
|
||||
detail="Unable to determine file type",
|
||||
)
|
||||
detected_content_type = file_info.extension.lower()
|
||||
if (
|
||||
file.content_type not in settings.lnbits_upload_allowed_types
|
||||
or detected_content_type not in settings.lnbits_upload_allowed_types
|
||||
):
|
||||
raise HTTPException(HTTPStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported file type")
|
||||
|
||||
# validate file name
|
||||
try:
|
||||
file_path = safe_upload_file_path(file.filename)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail=f"The requested filename '{file.filename}' is forbidden.",
|
||||
) from e
|
||||
|
||||
# validate file size
|
||||
real_file_size = 0
|
||||
temp: IO = NamedTemporaryFile(delete=False)
|
||||
for chunk in file.file:
|
||||
real_file_size += len(chunk)
|
||||
if real_file_size > content_length:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File too large ({content_length / 1000} KB max)",
|
||||
)
|
||||
temp.write(chunk)
|
||||
temp.close()
|
||||
|
||||
move(temp.name, file_path)
|
||||
return Image(filename=file.filename)
|
||||
|
||||
|
||||
@admin_router.get(
|
||||
"/api/v1/images",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def list_uploaded_images() -> list[Image]:
|
||||
image_folder = Path(settings.lnbits_data_folder, "images")
|
||||
files = image_folder.glob("*")
|
||||
images = []
|
||||
for file in files:
|
||||
if file.is_file():
|
||||
images.append(Image(filename=file.name))
|
||||
return images
|
||||
|
||||
|
||||
@admin_router.delete(
|
||||
"/api/v1/images/{filename}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_admin)],
|
||||
)
|
||||
async def delete_uploaded_image(filename: str) -> SimpleStatus:
|
||||
try:
|
||||
file_path = safe_upload_file_path(filename)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail=f"The requested filename '{filename}' is forbidden.",
|
||||
) from e
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Image not found.")
|
||||
|
||||
file_path.unlink()
|
||||
return SimpleStatus(success=True, message=f"{filename} deleted")
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import base64
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
||||
|
||||
from lnbits.core.crud.assets import (
|
||||
delete_user_asset,
|
||||
get_asset_info,
|
||||
get_public_asset,
|
||||
get_public_asset_info,
|
||||
get_user_asset,
|
||||
get_user_asset_info,
|
||||
get_user_assets,
|
||||
update_user_asset_info,
|
||||
)
|
||||
from lnbits.core.models.assets import AssetFilters, AssetInfo, AssetUpdate
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.core.services.assets import create_user_asset
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
check_user_exists,
|
||||
optional_user_id,
|
||||
parse_filters,
|
||||
)
|
||||
|
||||
asset_router = APIRouter(prefix="/api/v1/assets", tags=["Assets"])
|
||||
|
||||
upload_file_param = File(...)
|
||||
|
||||
|
||||
@asset_router.get(
|
||||
"/paginated",
|
||||
name="Get user assets",
|
||||
summary="Get paginated list user assets",
|
||||
)
|
||||
async def api_get_user_assets(
|
||||
user: User = Depends(check_user_exists),
|
||||
filters: Filters = Depends(parse_filters(AssetFilters)),
|
||||
) -> Page[AssetInfo]:
|
||||
return await get_user_assets(user.id, filters=filters)
|
||||
|
||||
|
||||
@asset_router.get(
|
||||
"/{asset_id}",
|
||||
name="Get user asset",
|
||||
summary="Get user asset by ID",
|
||||
)
|
||||
async def api_get_asset(
|
||||
asset_id: str,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> AssetInfo:
|
||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||
if not asset_info:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
return asset_info
|
||||
|
||||
|
||||
@asset_router.get(
|
||||
"/{asset_id}/binary",
|
||||
name="Get user asset binary",
|
||||
summary="Get user asset binary data by ID",
|
||||
)
|
||||
async def api_get_asset_binary(
|
||||
asset_id: str,
|
||||
user_id: str | None = Depends(optional_user_id),
|
||||
) -> Response:
|
||||
asset = None
|
||||
if user_id:
|
||||
asset = await get_user_asset(user_id, asset_id)
|
||||
|
||||
if not asset:
|
||||
asset = await get_public_asset(asset_id)
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
|
||||
return Response(
|
||||
content=asset.data,
|
||||
media_type=asset.mime_type,
|
||||
headers={"Content-Disposition": f'inline; filename="{asset.name}"'},
|
||||
)
|
||||
|
||||
|
||||
@asset_router.get(
|
||||
"/{asset_id}/thumbnail",
|
||||
name="Get user asset thumbnail",
|
||||
summary="Get user asset thumbnail data by ID",
|
||||
)
|
||||
async def api_get_asset_thumbnail(
|
||||
asset_id: str,
|
||||
user_id: str | None = Depends(optional_user_id),
|
||||
) -> Response:
|
||||
asset_info = None
|
||||
if user_id:
|
||||
asset_info = await get_user_asset_info(user_id, asset_id)
|
||||
|
||||
if not asset_info:
|
||||
asset_info = await get_public_asset_info(asset_id)
|
||||
|
||||
if not asset_info:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
|
||||
return Response(
|
||||
content=(
|
||||
base64.b64decode(asset_info.thumbnail_base64)
|
||||
if asset_info.thumbnail_base64
|
||||
else b""
|
||||
),
|
||||
media_type=asset_info.mime_type,
|
||||
headers={"Content-Disposition": f'inline; filename="{asset_info.name}"'},
|
||||
)
|
||||
|
||||
|
||||
@asset_router.put(
|
||||
"/{asset_id}",
|
||||
name="Update user asset",
|
||||
summary="Update user asset by ID",
|
||||
)
|
||||
async def api_update_asset(
|
||||
asset_id: str,
|
||||
data: AssetUpdate,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> AssetInfo:
|
||||
if user.admin:
|
||||
asset_info = await get_asset_info(asset_id)
|
||||
else:
|
||||
asset_info = await get_user_asset_info(user.id, asset_id)
|
||||
|
||||
if not asset_info:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
|
||||
asset_info.name = data.name or asset_info.name
|
||||
asset_info.is_public = (
|
||||
asset_info.is_public if data.is_public is None else data.is_public
|
||||
)
|
||||
await update_user_asset_info(asset_info)
|
||||
return asset_info
|
||||
|
||||
|
||||
@asset_router.post(
|
||||
"",
|
||||
name="Upload",
|
||||
summary="Upload user assets",
|
||||
)
|
||||
async def api_upload_asset(
|
||||
user: User = Depends(check_user_exists),
|
||||
file: UploadFile = upload_file_param,
|
||||
public_asset: bool = False,
|
||||
) -> AssetInfo:
|
||||
asset = await create_user_asset(user.id, file, public_asset)
|
||||
|
||||
asset_info = await get_user_asset_info(user.id, asset.id)
|
||||
if not asset_info:
|
||||
raise ValueError("Failed to retrieve asset info after upload.")
|
||||
|
||||
return asset_info
|
||||
|
||||
|
||||
@asset_router.delete(
|
||||
"/{asset_id}",
|
||||
name="Delete user asset",
|
||||
summary="Delete user asset by ID",
|
||||
)
|
||||
async def api_delete_asset(
|
||||
asset_id: str,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> SimpleStatus:
|
||||
asset = await get_user_asset(user.id, asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Asset not found.")
|
||||
|
||||
await delete_user_asset(user.id, asset_id)
|
||||
return SimpleStatus(success=True, message="Asset deleted successfully.")
|
||||
@@ -25,6 +25,7 @@ from lnbits.core.models.users import (
|
||||
UpdateAccessControlList,
|
||||
)
|
||||
from lnbits.core.services import create_user_account
|
||||
from lnbits.core.services.users import update_user_account
|
||||
from lnbits.decorators import access_token_payload, check_user_exists
|
||||
from lnbits.helpers import (
|
||||
create_access_token,
|
||||
@@ -422,15 +423,6 @@ async def update(
|
||||
) -> User | None:
|
||||
if data.user_id != user.id:
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
|
||||
if data.username and not is_valid_username(data.username):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
|
||||
|
||||
if (
|
||||
data.username
|
||||
and user.username != data.username
|
||||
and await get_account_by_username(data.username)
|
||||
):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
|
||||
|
||||
account = await get_account(user.id)
|
||||
if not account:
|
||||
@@ -441,7 +433,7 @@ async def update(
|
||||
if data.extra:
|
||||
account.extra = data.extra
|
||||
|
||||
await update_account(account)
|
||||
await update_user_account(account)
|
||||
return await get_user_from_account(account)
|
||||
|
||||
|
||||
|
||||
@@ -36,12 +36,14 @@ from lnbits.decorators import (
|
||||
check_admin,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..crud import (
|
||||
create_user_extension,
|
||||
delete_dbversion,
|
||||
drop_extension_db,
|
||||
get_db_version,
|
||||
get_db_versions,
|
||||
get_installed_extension,
|
||||
get_installed_extensions,
|
||||
get_user_extension,
|
||||
@@ -492,3 +494,82 @@ async def delete_extension_db(ext_id: str):
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||
) from exc
|
||||
|
||||
|
||||
# TODO: create a response model for this
|
||||
@extension_router.get("/all")
|
||||
async def extensions(user: User = Depends(check_user_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||
installable_exts_ids = [e.id for e in installable_exts]
|
||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||
|
||||
for e in installable_exts:
|
||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||
if installed_ext and installed_ext.meta:
|
||||
installed_release = installed_ext.meta.installed_release
|
||||
if installed_ext.meta.pay_to_enable and not user.admin:
|
||||
# not a security leak, but better not to share the wallet id
|
||||
installed_ext.meta.pay_to_enable.wallet = None
|
||||
pay_to_enable = installed_ext.meta.pay_to_enable
|
||||
|
||||
if e.meta:
|
||||
e.meta.installed_release = installed_release
|
||||
e.meta.pay_to_enable = pay_to_enable
|
||||
else:
|
||||
e.meta = ExtensionMeta(
|
||||
installed_release=installed_release,
|
||||
pay_to_enable=pay_to_enable,
|
||||
)
|
||||
# use the installed extension values
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
|
||||
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
||||
db_versions = await get_db_versions()
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
"id": ext.id,
|
||||
"name": ext.name,
|
||||
"icon": ext.icon,
|
||||
"shortDescription": ext.short_description,
|
||||
"stars": ext.stars,
|
||||
"isFeatured": ext.meta.featured if ext.meta else False,
|
||||
"dependencies": ext.meta.dependencies if ext.meta else "",
|
||||
"isInstalled": ext.id in installed_exts_ids,
|
||||
"hasDatabaseTables": next(
|
||||
(True for version in db_versions if version.db == ext.id), False
|
||||
),
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": (
|
||||
dict(ext.meta.latest_release)
|
||||
if ext.meta and ext.meta.latest_release
|
||||
else None
|
||||
),
|
||||
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
||||
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
||||
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
||||
"installedRelease": (
|
||||
dict(ext.meta.installed_release)
|
||||
if ext.meta and ext.meta.installed_release
|
||||
else None
|
||||
),
|
||||
"payToEnable": (
|
||||
dict(ext.meta.pay_to_enable)
|
||||
if ext.meta and ext.meta.pay_to_enable
|
||||
else {}
|
||||
),
|
||||
"isPaymentRequired": ext.requires_payment,
|
||||
"inProgress": False,
|
||||
"selectedForUpdate": False,
|
||||
}
|
||||
for ext in installable_exts
|
||||
]
|
||||
return extension_data
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import shutil
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import (
|
||||
@@ -28,9 +27,9 @@ from lnbits.core.services.extensions_builder import (
|
||||
)
|
||||
from lnbits.decorators import (
|
||||
check_admin,
|
||||
check_extension_builder,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.settings import settings
|
||||
|
||||
from ..crud import (
|
||||
create_user_extension,
|
||||
@@ -47,19 +46,12 @@ extension_builder_router = APIRouter(
|
||||
@extension_builder_router.post(
|
||||
"/zip",
|
||||
summary="Build and download extension zip.",
|
||||
dependencies=[Depends(check_extension_builder)],
|
||||
description="""
|
||||
This endpoint generates a zip file for the extension based on the provided data.
|
||||
""",
|
||||
)
|
||||
async def api_build_extension(
|
||||
data: ExtensionData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> FileResponse:
|
||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
"Extension Builder is disabled for non admin users.",
|
||||
)
|
||||
async def api_build_extension(data: ExtensionData) -> FileResponse:
|
||||
stub_ext_id = "extension_builder_stub" # todo: do not hardcode, fetch from manifest
|
||||
release, build_dir = await build_extension_from_data(data, stub_ext_id)
|
||||
|
||||
@@ -132,16 +124,12 @@ async def api_deploy_extension(
|
||||
@extension_builder_router.post(
|
||||
"/preview",
|
||||
summary="Build and preview the extension ui.",
|
||||
dependencies=[Depends(check_extension_builder)],
|
||||
)
|
||||
async def api_preview_extension(
|
||||
data: ExtensionData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> SimpleStatus:
|
||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
"Extension Builder is disabled for non admin users.",
|
||||
)
|
||||
stub_ext_id = "extension_builder_stub"
|
||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
||||
|
||||
+35
-335
@@ -1,35 +1,29 @@
|
||||
from hashlib import sha256
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Cookie, Depends, Query, Request
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from lnurl import url_decode
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.helpers import to_valid_user_id
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.extensions import ExtensionMeta, InstallableExtension
|
||||
from lnbits.core.services import create_invoice, create_user_account
|
||||
from lnbits.core.services.extensions import get_valid_extensions
|
||||
from lnbits.decorators import check_admin, check_user_exists
|
||||
from lnbits.decorators import (
|
||||
check_admin,
|
||||
check_admin_ui,
|
||||
check_extension_builder,
|
||||
check_first_install,
|
||||
check_user_exists,
|
||||
)
|
||||
from lnbits.helpers import check_callback_url, template_renderer
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_funding_source
|
||||
|
||||
from ...utils.exchange_rates import allowed_currencies, currencies
|
||||
from ..crud import (
|
||||
create_wallet,
|
||||
get_db_versions,
|
||||
get_installed_extensions,
|
||||
get_user,
|
||||
get_wallet,
|
||||
)
|
||||
from ..crud import get_user
|
||||
|
||||
generic_router = APIRouter(
|
||||
tags=["Core NON-API Website Routes"], include_in_schema=False
|
||||
@@ -41,251 +35,23 @@ async def favicon():
|
||||
return RedirectResponse(settings.lnbits_qr_logo)
|
||||
|
||||
|
||||
@generic_router.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request, lightning: str = ""):
|
||||
return template_renderer().TemplateResponse(
|
||||
request, "core/index.html", {"lnurl": lightning}
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/account",
|
||||
response_class=HTMLResponse,
|
||||
description="show account page",
|
||||
)
|
||||
async def account(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
nostr_configured = settings.is_nostr_notifications_configured()
|
||||
telegram_configured = settings.is_telegram_notifications_configured()
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/account.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"nostr_configured": nostr_configured,
|
||||
"telegram_configured": telegram_configured,
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/wallet",
|
||||
response_class=HTMLResponse,
|
||||
description="show wallet page",
|
||||
)
|
||||
async def get_user_wallet(
|
||||
request: Request,
|
||||
lnbits_last_active_wallet: Annotated[str | None, Cookie()] = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
wal: UUID4 | None = Query(None),
|
||||
):
|
||||
if wal:
|
||||
wallet = await get_wallet(wal.hex)
|
||||
elif len(user.wallets) == 0:
|
||||
wallet = await create_wallet(user_id=user.id)
|
||||
user.wallets.append(wallet)
|
||||
elif lnbits_last_active_wallet and user.get_wallet(lnbits_last_active_wallet):
|
||||
wallet = await get_wallet(lnbits_last_active_wallet)
|
||||
else:
|
||||
wallet = user.wallets[0]
|
||||
|
||||
if not wallet or wallet.deleted:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Wallet not found",
|
||||
)
|
||||
if wallet.user != user.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Not your wallet.",
|
||||
)
|
||||
context = {
|
||||
"user": user.json(),
|
||||
"wallet": wallet.json(),
|
||||
"wallet_name": wallet.name,
|
||||
"currencies": allowed_currencies(),
|
||||
"service_fee": settings.lnbits_service_fee,
|
||||
"service_fee_max": settings.lnbits_service_fee_max,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
}
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/wallet.html",
|
||||
{**context, "ajax": _is_ajax_request(request)},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/wallets",
|
||||
response_class=HTMLResponse,
|
||||
description="show wallets page",
|
||||
)
|
||||
async def wallets(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/wallets.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/first_install", response_class=HTMLResponse)
|
||||
async def first_install(request: Request):
|
||||
if not settings.first_install:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Super user account has already been configured.",
|
||||
)
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/first_install.html",
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||
async def robots():
|
||||
data = """
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
"""
|
||||
data = "User-agent: *\nDisallow: /"
|
||||
return HTMLResponse(content=data, media_type="text/plain")
|
||||
|
||||
|
||||
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
|
||||
async def extensions(request: Request, user: User = Depends(check_user_exists)):
|
||||
installed_exts: list[InstallableExtension] = await get_installed_extensions()
|
||||
installed_exts_ids = [e.id for e in installed_exts]
|
||||
|
||||
installable_exts = await InstallableExtension.get_installable_extensions()
|
||||
installable_exts_ids = [e.id for e in installable_exts]
|
||||
installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
|
||||
|
||||
for e in installable_exts:
|
||||
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
|
||||
if installed_ext and installed_ext.meta:
|
||||
installed_release = installed_ext.meta.installed_release
|
||||
if installed_ext.meta.pay_to_enable and not user.admin:
|
||||
# not a security leak, but better not to share the wallet id
|
||||
installed_ext.meta.pay_to_enable.wallet = None
|
||||
pay_to_enable = installed_ext.meta.pay_to_enable
|
||||
|
||||
if e.meta:
|
||||
e.meta.installed_release = installed_release
|
||||
e.meta.pay_to_enable = pay_to_enable
|
||||
else:
|
||||
e.meta = ExtensionMeta(
|
||||
installed_release=installed_release,
|
||||
pay_to_enable=pay_to_enable,
|
||||
)
|
||||
# use the installed extension values
|
||||
e.name = installed_ext.name
|
||||
e.short_description = installed_ext.short_description
|
||||
e.icon = installed_ext.icon
|
||||
|
||||
all_ext_ids = [ext.code for ext in await get_valid_extensions()]
|
||||
inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
|
||||
db_versions = await get_db_versions()
|
||||
|
||||
extension_data = [
|
||||
{
|
||||
"id": ext.id,
|
||||
"name": ext.name,
|
||||
"icon": ext.icon,
|
||||
"shortDescription": ext.short_description,
|
||||
"stars": ext.stars,
|
||||
"isFeatured": ext.meta.featured if ext.meta else False,
|
||||
"dependencies": ext.meta.dependencies if ext.meta else "",
|
||||
"isInstalled": ext.id in installed_exts_ids,
|
||||
"hasDatabaseTables": next(
|
||||
(True for version in db_versions if version.db == ext.id), False
|
||||
),
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": (
|
||||
dict(ext.meta.latest_release)
|
||||
if ext.meta and ext.meta.latest_release
|
||||
else None
|
||||
),
|
||||
"hasPaidRelease": ext.meta.has_paid_release if ext.meta else False,
|
||||
"hasFreeRelease": ext.meta.has_free_release if ext.meta else False,
|
||||
"paidFeatures": ext.meta.paid_features if ext.meta else False,
|
||||
"installedRelease": (
|
||||
dict(ext.meta.installed_release)
|
||||
if ext.meta and ext.meta.installed_release
|
||||
else None
|
||||
),
|
||||
"payToEnable": (
|
||||
dict(ext.meta.pay_to_enable)
|
||||
if ext.meta and ext.meta.pay_to_enable
|
||||
else {}
|
||||
),
|
||||
"isPaymentRequired": ext.requires_payment,
|
||||
}
|
||||
for ext in installable_exts
|
||||
]
|
||||
|
||||
# refresh user state. Eg: enabled extensions.
|
||||
# TODO: refactor
|
||||
# user = await get_user(user.id) or user
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/extensions.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"extension_data": extension_data,
|
||||
"extension_builder_enabled": user.admin
|
||||
or settings.lnbits_extensions_builder_activate_non_admins,
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/extensions/builder", name="extensions builder", response_class=HTMLResponse
|
||||
)
|
||||
async def extensions_builder(request: Request, user: User = Depends(check_user_exists)):
|
||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
"Extension Builder is disabled for non admin users.",
|
||||
)
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"core/extensions_builder.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get(
|
||||
"/extensions/builder/preview/{ext_id}",
|
||||
name="extensions builder",
|
||||
response_class=HTMLResponse,
|
||||
dependencies=[Depends(check_extension_builder)],
|
||||
)
|
||||
async def extensions_builder_preview(
|
||||
request: Request,
|
||||
ext_id: str,
|
||||
page_name: str | None = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
):
|
||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
"Extension Builder is disabled for non admin users.",
|
||||
)
|
||||
) -> HTMLResponse:
|
||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||
html_file_name = "index.html"
|
||||
if page_name == "public_page":
|
||||
@@ -359,7 +125,7 @@ async def manifest(request: Request, usr: str):
|
||||
"src": (
|
||||
settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
|
||||
else "images/logos/lnbits.png"
|
||||
),
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
@@ -426,104 +192,38 @@ async def manifest(request: Request, usr: str):
|
||||
}
|
||||
|
||||
|
||||
@generic_router.get("/node", response_class=HTMLResponse)
|
||||
async def node(request: Request, user: User = Depends(check_admin)):
|
||||
if not settings.lnbits_node_ui:
|
||||
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
|
||||
admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
||||
|
||||
funding_source = get_funding_source()
|
||||
_, balance = await funding_source.status()
|
||||
|
||||
@generic_router.get("/payments")
|
||||
@generic_router.get("/wallet")
|
||||
@generic_router.get("/wallets")
|
||||
@generic_router.get("/account")
|
||||
@generic_router.get("/extensions")
|
||||
@generic_router.get("/users", dependencies=admin_ui_checks)
|
||||
@generic_router.get("/audit", dependencies=admin_ui_checks)
|
||||
@generic_router.get("/node", dependencies=admin_ui_checks)
|
||||
@generic_router.get("/admin", dependencies=admin_ui_checks)
|
||||
@generic_router.get(
|
||||
"/extensions/builder", dependencies=[Depends(check_extension_builder)]
|
||||
)
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists)
|
||||
) -> HTMLResponse:
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"node/index.html",
|
||||
"index.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"balance": balance,
|
||||
"wallets": user.wallets[0].json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/node/public", response_class=HTMLResponse)
|
||||
async def node_public(request: Request):
|
||||
if not settings.lnbits_public_node_ui:
|
||||
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
|
||||
|
||||
funding_source = get_funding_source()
|
||||
_, balance = await funding_source.status()
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"node/public.html",
|
||||
{
|
||||
"balance": balance,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/admin", response_class=HTMLResponse)
|
||||
async def admin_index(request: Request, user: User = Depends(check_admin)):
|
||||
if not settings.lnbits_admin_ui:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
funding_source = get_funding_source()
|
||||
_, balance = await funding_source.status()
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
request,
|
||||
"admin/index.html",
|
||||
{
|
||||
"user": user.json(),
|
||||
"balance": balance,
|
||||
"currencies": list(currencies.keys()),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/users", response_class=HTMLResponse)
|
||||
async def users_index(request: Request, user: User = Depends(check_admin)):
|
||||
if not settings.lnbits_admin_ui:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"users/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.json(),
|
||||
"currencies": list(currencies.keys()),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/audit", response_class=HTMLResponse)
|
||||
async def audit_index(request: Request, user: User = Depends(check_admin)):
|
||||
if not settings.lnbits_audit_enabled:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Audit not enabled")
|
||||
|
||||
return template_renderer().TemplateResponse(
|
||||
"audit/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@generic_router.get("/payments", response_class=HTMLResponse)
|
||||
async def payments_index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return template_renderer().TemplateResponse(
|
||||
"payments/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"user": user.json(),
|
||||
"ajax": _is_ajax_request(request),
|
||||
},
|
||||
)
|
||||
@generic_router.get("/")
|
||||
@generic_router.get("/node/public")
|
||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
||||
async def index_public(request: Request) -> HTMLResponse:
|
||||
return template_renderer().TemplateResponse(request, "index_public.html")
|
||||
|
||||
|
||||
@generic_router.get("/uuidv4/{hex_value}")
|
||||
|
||||
@@ -6,16 +6,17 @@ from fastapi import (
|
||||
Depends,
|
||||
HTTPException,
|
||||
)
|
||||
from lnurl import LnurlResponseException
|
||||
from lnurl import execute_login as lnurlauth
|
||||
from lnurl import handle as lnurl_handle
|
||||
from lnurl.models import (
|
||||
from lnurl import (
|
||||
LnurlAuthResponse,
|
||||
LnurlErrorResponse,
|
||||
LnurlException,
|
||||
LnurlPayResponse,
|
||||
LnurlResponseModel,
|
||||
LnurlResponseException,
|
||||
LnurlWithdrawResponse,
|
||||
)
|
||||
from lnurl import execute_login as lnurlauth
|
||||
from lnurl import handle as lnurl_handle
|
||||
from lnurl.models import LnurlResponseModel
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
@@ -38,7 +39,7 @@ async def _handle(lnurl: str) -> LnurlResponseModel:
|
||||
res = await lnurl_handle(lnurl, user_agent=settings.user_agent, timeout=5)
|
||||
if isinstance(res, LnurlErrorResponse):
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=res.reason)
|
||||
except LnurlResponseException as exc:
|
||||
except LnurlException as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||
) from exc
|
||||
|
||||
@@ -3,7 +3,6 @@ from http import HTTPStatus
|
||||
import httpx
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from starlette.status import HTTP_503_SERVICE_UNAVAILABLE
|
||||
|
||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||
from lnbits.settings import settings
|
||||
@@ -154,7 +153,7 @@ async def api_get_payments(
|
||||
) -> Page[NodePayment] | None:
|
||||
if not settings.lnbits_node_ui_transactions:
|
||||
raise HTTPException(
|
||||
HTTP_503_SERVICE_UNAVAILABLE,
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail="You can enable node transactions in the Admin UI",
|
||||
)
|
||||
return await node.get_payments(filters)
|
||||
@@ -167,7 +166,7 @@ async def api_get_invoices(
|
||||
) -> Page[NodeInvoice] | None:
|
||||
if not settings.lnbits_node_ui_transactions:
|
||||
raise HTTPException(
|
||||
HTTP_503_SERVICE_UNAVAILABLE,
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail="You can enable node transactions in the Admin UI",
|
||||
)
|
||||
return await node.get_invoices(filters)
|
||||
|
||||
@@ -15,7 +15,9 @@ from lnbits import bolt11
|
||||
from lnbits.core.crud.payments import (
|
||||
get_payment_count_stats,
|
||||
get_wallets_stats,
|
||||
update_payment,
|
||||
)
|
||||
from lnbits.core.crud.users import get_account
|
||||
from lnbits.core.models import (
|
||||
CancelInvoice,
|
||||
CreateInvoice,
|
||||
@@ -32,6 +34,7 @@ from lnbits.core.models import (
|
||||
SettleInvoice,
|
||||
SimpleStatus,
|
||||
)
|
||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
||||
from lnbits.core.models.users import User
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
@@ -176,12 +179,18 @@ async def api_payments_daily_stats(
|
||||
)
|
||||
async def api_payments_paginated(
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
recheck_pending: bool = Query(
|
||||
False, description="Force check and update of pending payments."
|
||||
),
|
||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||
):
|
||||
page = await get_payments_paginated(
|
||||
wallet_id=key_info.wallet.id,
|
||||
filters=filters,
|
||||
)
|
||||
if not recheck_pending:
|
||||
return page
|
||||
|
||||
for payment in page.data:
|
||||
if payment.pending:
|
||||
await update_pending_payment(payment)
|
||||
@@ -247,6 +256,7 @@ async def api_payments_create(
|
||||
wallet_id=wallet_id,
|
||||
payment_request=invoice_data.bolt11,
|
||||
extra=invoice_data.extra,
|
||||
labels=invoice_data.labels,
|
||||
)
|
||||
return payment
|
||||
|
||||
@@ -260,6 +270,27 @@ async def api_payments_create(
|
||||
return await create_payment_request(wallet_id, invoice_data)
|
||||
|
||||
|
||||
@payment_router.put("/{payment_hash}/labels")
|
||||
async def api_update_payment_labels(
|
||||
payment_hash: str,
|
||||
data: UpdatePaymentLabels,
|
||||
key_type: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> SimpleStatus:
|
||||
|
||||
payment = await get_standalone_payment(payment_hash, wallet_id=key_type.wallet.id)
|
||||
if payment is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Payment does not exist.")
|
||||
account = await get_account(key_type.wallet.user)
|
||||
if not account:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Account does not exist.")
|
||||
|
||||
# only keep labels that belong to the user
|
||||
user_label_names = [label.name for label in account.extra.labels]
|
||||
payment.labels = [label for label in data.labels if label in user_label_names]
|
||||
await update_payment(payment)
|
||||
return SimpleStatus(success=True, message="Payment labels updated.")
|
||||
|
||||
|
||||
@payment_router.get("/fee-reserve")
|
||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
|
||||
@@ -8,10 +8,25 @@ from fastapi import (
|
||||
HTTPException,
|
||||
)
|
||||
|
||||
from lnbits.core.crud.wallets import get_wallets_paginated
|
||||
from lnbits.core.crud.wallets import (
|
||||
create_wallet,
|
||||
get_wallets_paginated,
|
||||
)
|
||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||
from lnbits.core.models.wallets import WalletsFilters
|
||||
from lnbits.core.models.misc import SimpleStatus
|
||||
from lnbits.core.models.wallets import (
|
||||
WalletsFilters,
|
||||
WalletSharePermission,
|
||||
WalletType,
|
||||
)
|
||||
from lnbits.core.services.wallets import (
|
||||
create_lightning_shared_wallet,
|
||||
delete_wallet_share,
|
||||
invite_to_wallet,
|
||||
reject_wallet_invitation,
|
||||
update_wallet_share_permissions,
|
||||
)
|
||||
from lnbits.db import Filters, Page
|
||||
from lnbits.decorators import (
|
||||
check_user_exists,
|
||||
@@ -22,7 +37,6 @@ from lnbits.decorators import (
|
||||
from lnbits.helpers import generate_filter_params_openapi
|
||||
|
||||
from ..crud import (
|
||||
create_wallet,
|
||||
delete_wallet,
|
||||
get_wallet,
|
||||
update_wallet,
|
||||
@@ -62,6 +76,35 @@ async def api_wallets_paginated(
|
||||
return page
|
||||
|
||||
|
||||
@wallet_router.put("/share/invite")
|
||||
async def api_invite_wallet_share(
|
||||
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> WalletSharePermission:
|
||||
return await invite_to_wallet(key_info.wallet, data)
|
||||
|
||||
|
||||
@wallet_router.delete("/share/invite/{share_request_id}")
|
||||
async def api_reject_wallet_invitation(
|
||||
share_request_id: str, invited_user: User = Depends(check_user_exists)
|
||||
) -> SimpleStatus:
|
||||
await reject_wallet_invitation(invited_user.id, share_request_id)
|
||||
return SimpleStatus(success=True, message="Invitation rejected.")
|
||||
|
||||
|
||||
@wallet_router.put("/share")
|
||||
async def api_accept_wallet_share_request(
|
||||
data: WalletSharePermission, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> WalletSharePermission:
|
||||
return await update_wallet_share_permissions(key_info.wallet, data)
|
||||
|
||||
|
||||
@wallet_router.delete("/share/{share_request_id}")
|
||||
async def api_delete_wallet_share_permissions(
|
||||
share_request_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> SimpleStatus:
|
||||
return await delete_wallet_share(key_info.wallet, share_request_id)
|
||||
|
||||
|
||||
@wallet_router.put("/{new_name}")
|
||||
async def api_update_wallet_name(
|
||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||
@@ -70,6 +113,7 @@ async def api_update_wallet_name(
|
||||
if not wallet:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||
wallet.name = new_name
|
||||
|
||||
await update_wallet(wallet)
|
||||
return {
|
||||
"id": wallet.id,
|
||||
@@ -124,6 +168,7 @@ async def api_update_wallet(
|
||||
wallet.extra.color = color or wallet.extra.color
|
||||
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
||||
wallet.currency = currency if currency is not None else wallet.currency
|
||||
|
||||
await update_wallet(wallet)
|
||||
return wallet
|
||||
|
||||
@@ -147,4 +192,21 @@ async def api_create_wallet(
|
||||
data: CreateWallet,
|
||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> Wallet:
|
||||
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
||||
if data.wallet_type == WalletType.LIGHTNING:
|
||||
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
||||
|
||||
if data.wallet_type == WalletType.LIGHTNING_SHARED:
|
||||
if not data.shared_wallet_id:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"Shared wallet ID is required for shared wallets.",
|
||||
)
|
||||
return await create_lightning_shared_wallet(
|
||||
user_id=key_info.wallet.user,
|
||||
source_wallet_id=data.shared_wallet_id,
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
f"Unknown wallet type: {data.wallet_type}.",
|
||||
)
|
||||
|
||||
+92
-25
@@ -130,6 +130,12 @@ class Compat:
|
||||
return "BIGINT"
|
||||
return "INT"
|
||||
|
||||
@property
|
||||
def blob(self) -> str:
|
||||
if self.type in {POSTGRES}:
|
||||
return "BYTEA"
|
||||
return "BLOB"
|
||||
|
||||
def timestamp_placeholder(self, key: str) -> str:
|
||||
return compat_timestamp_placeholder(key)
|
||||
|
||||
@@ -216,18 +222,34 @@ class Connection(Compat):
|
||||
filters: Filters | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
group_by: list[str] | None = None,
|
||||
table_name: str | None = None,
|
||||
) -> Page[TModel]:
|
||||
"""
|
||||
Parameters:
|
||||
query: The main SQL query string to execute for data retrieval.
|
||||
where: list of additional WHERE clause conditions to filter results.
|
||||
values: dictionary of parameter values to be used in the SQL query.
|
||||
filters: object for advanced filtering, sorting, and pagination logic.
|
||||
model: pydantic model type to map query results into model instances.
|
||||
group_by: list of column names to group results by in the SQL query.
|
||||
table_name: if provided some optimisations can be applied.
|
||||
"""
|
||||
|
||||
if not filters:
|
||||
filters = Filters()
|
||||
|
||||
if table_name:
|
||||
if not _valid_sql_name(table_name):
|
||||
raise ValueError(f"Invalid table name: '{table_name}'.")
|
||||
filters.set_table_name(table_name)
|
||||
|
||||
clause = filters.where(where)
|
||||
parsed_values = filters.values(values)
|
||||
|
||||
group_by_string = ""
|
||||
if group_by:
|
||||
for field in group_by:
|
||||
if not re.fullmatch(
|
||||
r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", field
|
||||
):
|
||||
if not _valid_sql_name(field):
|
||||
raise ValueError("Value for GROUP BY is invalid")
|
||||
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
||||
|
||||
@@ -245,16 +267,17 @@ class Connection(Compat):
|
||||
if rows:
|
||||
# no need for extra query if no pagination is specified
|
||||
if filters.offset or filters.limit:
|
||||
result = await self.execute(
|
||||
f"""
|
||||
SELECT COUNT(*) as count FROM (
|
||||
if table_name:
|
||||
count_query = f"SELECT COUNT(*) as count FROM {table_name} {clause}" # noqa: S608
|
||||
else:
|
||||
count_query = f"""SELECT COUNT(*) as count
|
||||
FROM (
|
||||
{query}
|
||||
{clause}
|
||||
{group_by_string}
|
||||
) as count
|
||||
""", # noqa: S608
|
||||
parsed_values,
|
||||
)
|
||||
) as count""" # noqa: S608
|
||||
|
||||
result = await self.execute(count_query, parsed_values)
|
||||
row = result.mappings().first()
|
||||
result.close()
|
||||
count = int(row.get("count", 0))
|
||||
@@ -387,9 +410,12 @@ class Database(Compat):
|
||||
filters: Filters | None = None,
|
||||
model: type[TModel] | None = None,
|
||||
group_by: list[str] | None = None,
|
||||
table_name: str | None = None,
|
||||
) -> Page[TModel]:
|
||||
async with self.connect() as conn:
|
||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||
return await conn.fetch_page(
|
||||
query, where, values, filters, model, group_by, table_name
|
||||
)
|
||||
|
||||
async def execute(self, query: str, values: dict | None = None):
|
||||
async with self.connect() as conn:
|
||||
@@ -425,6 +451,8 @@ class Operator(Enum):
|
||||
INCLUDE = "in"
|
||||
EXCLUDE = "ex"
|
||||
LIKE = "like"
|
||||
EVERY = "every"
|
||||
ANY = "any"
|
||||
|
||||
@property
|
||||
def as_sql(self):
|
||||
@@ -444,7 +472,7 @@ class Operator(Enum):
|
||||
return ">="
|
||||
elif self == Operator.LE:
|
||||
return "<="
|
||||
elif self == Operator.LIKE:
|
||||
elif self in {Operator.LIKE, Operator.EVERY, Operator.ANY}:
|
||||
return "LIKE"
|
||||
else:
|
||||
raise ValueError("Unknown SQL Operator")
|
||||
@@ -466,6 +494,7 @@ class Page(BaseModel, Generic[T]):
|
||||
|
||||
|
||||
class Filter(BaseModel, Generic[TFilterModel]):
|
||||
table_name: str | None = None
|
||||
field: str
|
||||
op: Operator = Operator.EQ
|
||||
model: type[TFilterModel] | None
|
||||
@@ -475,6 +504,8 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
def parse_query(
|
||||
cls, key: str, raw_values: list[Any], model: type[TFilterModel], i: int = 0
|
||||
):
|
||||
if i > 1000 or len(raw_values) > 1000:
|
||||
raise ValueError("Too many filter values")
|
||||
# Key format:
|
||||
# key[operator]
|
||||
# e.g. name[eq]
|
||||
@@ -491,11 +522,14 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
if field in model.__fields__:
|
||||
compare_field = model.__fields__[field]
|
||||
values: dict = {}
|
||||
for raw_value in raw_values:
|
||||
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
raw_values = [v for rv in raw_values for v in rv.split(",")]
|
||||
|
||||
for index, raw_value in enumerate(raw_values):
|
||||
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
||||
if errors:
|
||||
raise ValidationError(errors=[errors], model=model)
|
||||
values[f"{field}__{i}"] = validated
|
||||
values[f"{field}__{index}"] = validated
|
||||
else:
|
||||
raise ValueError("Unknown filter field")
|
||||
|
||||
@@ -503,15 +537,24 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
@property
|
||||
def statement(self) -> str:
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
stmt = []
|
||||
for key in self.values.keys() if self.values else []:
|
||||
clean_key = key.split("__")[0]
|
||||
if self.model and self.model.__fields__[clean_key].type_ == datetime:
|
||||
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
||||
placeholder = compat_timestamp_placeholder(key)
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} {placeholder}")
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
stmt.append(f":{key}")
|
||||
else:
|
||||
placeholder = f":{key}"
|
||||
stmt.append(f"{clean_key} {self.op.as_sql} {placeholder}")
|
||||
return " OR ".join(stmt)
|
||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
||||
|
||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
||||
statement = f"{prefix}{self.field} {self.op.as_sql} ({', '.join(stmt)})"
|
||||
elif self.op == Operator.EVERY:
|
||||
statement = " AND ".join(stmt)
|
||||
else:
|
||||
statement = " OR ".join(stmt)
|
||||
return f"({statement})"
|
||||
|
||||
|
||||
class Filters(BaseModel, Generic[TFilterModel]):
|
||||
@@ -527,13 +570,14 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
search: str | None = None
|
||||
|
||||
offset: int | None = None
|
||||
limit: int | None = None
|
||||
|
||||
limit: int | None = 10
|
||||
sortby: str | None = None
|
||||
direction: Literal["asc", "desc"] | None = None
|
||||
|
||||
model: type[TFilterModel] | None = None
|
||||
|
||||
table_name: str | None = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def validate_sortby(cls, values):
|
||||
sortby = values.get("sortby")
|
||||
@@ -548,8 +592,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def pagination(self) -> str:
|
||||
stmt = ""
|
||||
if self.limit:
|
||||
stmt += f"LIMIT {self.limit} "
|
||||
self.limit = self.limit or 10
|
||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||
if self.offset:
|
||||
stmt += f"OFFSET {self.offset}"
|
||||
return stmt
|
||||
@@ -575,7 +619,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def order_by(self) -> str:
|
||||
if self.sortby:
|
||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||
prefix = f"{self.table_name}." if self.table_name else ""
|
||||
return f"ORDER BY {prefix}{self.sortby} {self.direction or 'asc'}"
|
||||
return ""
|
||||
|
||||
def values(self, values: dict | None = None) -> dict:
|
||||
@@ -587,12 +632,26 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
for key, value in page_filter.values.items():
|
||||
if page_filter.op == Operator.LIKE:
|
||||
values[key] = f"%{value}%"
|
||||
elif page_filter.op in {Operator.EVERY, Operator.ANY}:
|
||||
values[key] = f"""%"{value}"%"""
|
||||
else:
|
||||
values[key] = value
|
||||
if self.search and self.model:
|
||||
values["search"] = f"%{self.search.lower()}%"
|
||||
return values
|
||||
|
||||
def set_table_name(self, table_name: str) -> None:
|
||||
self.table_name = table_name
|
||||
for page_filter in self.filters:
|
||||
page_filter.table_name = table_name
|
||||
|
||||
|
||||
class DbJsonEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
|
||||
def insert_query(table_name: str, model: BaseModel) -> str:
|
||||
"""
|
||||
@@ -648,7 +707,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
||||
or type_ is dict
|
||||
or get_origin(outertype_) is list
|
||||
):
|
||||
_dict[key] = json.dumps(value)
|
||||
_dict[key] = json.dumps(value, cls=DbJsonEncoder)
|
||||
continue
|
||||
_dict[key] = value
|
||||
|
||||
@@ -722,3 +781,11 @@ def _safe_load_json(value: str) -> dict:
|
||||
# DB is corrupted if it gets here
|
||||
logger.error(f"Failed to decode JSON: '{value}'")
|
||||
return {}
|
||||
|
||||
|
||||
def _valid_sql_name(name: str) -> bool:
|
||||
"""Check if a SQL name is valid (alphanumeric and underscores only)"""
|
||||
return (
|
||||
re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", name)
|
||||
is not None
|
||||
)
|
||||
|
||||
@@ -359,3 +359,28 @@ def url_for_interceptor(original_method):
|
||||
# Upgraded extensions modify the path.
|
||||
# This interceptor ensures that the path is normalized.
|
||||
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
|
||||
|
||||
|
||||
async def check_admin_ui() -> None:
|
||||
if not settings.lnbits_admin_ui:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail="Admin UI is disabled."
|
||||
)
|
||||
|
||||
|
||||
async def check_extension_builder(
|
||||
user: Annotated[User, Depends(check_user_exists)],
|
||||
) -> None:
|
||||
if not settings.lnbits_extensions_builder_activate_non_admins and not user.admin:
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN,
|
||||
"Extension Builder is disabled for non admin users.",
|
||||
)
|
||||
|
||||
|
||||
async def check_first_install():
|
||||
if not settings.first_install:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Super user account has already been configured.",
|
||||
)
|
||||
|
||||
+25
-17
@@ -18,6 +18,7 @@ from pydantic.schema import field_schema
|
||||
from lnbits.jinja2_templating import Jinja2Templates
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.crypto import AESCipher
|
||||
from lnbits.utils.exchange_rates import currencies
|
||||
|
||||
from .db import FilterModel
|
||||
|
||||
@@ -69,7 +70,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
t.env.globals["static_url_for"] = static_url_for
|
||||
|
||||
window_settings = {
|
||||
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
||||
"AD_SPACE": settings.lnbits_ad_space,
|
||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||
@@ -97,15 +98,20 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||
"LNBITS_VERSION": settings.version,
|
||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||
"has_holdinvoice": settings.has_holdinvoice,
|
||||
"LNBITS_NOSTR_CONFIGURED": settings.is_nostr_notifications_configured(),
|
||||
"LNBITS_TELEGRAM_CONFIGURED": settings.is_telegram_notifications_configured(),
|
||||
"LNBITS_EXT_BUILDER": settings.lnbits_extensions_builder_activate_non_admins,
|
||||
"LNBITS_CURRENCIES": list(currencies.keys()),
|
||||
"LNBITS_ALLOWED_CURRENCIES": settings.lnbits_allowed_currencies,
|
||||
}
|
||||
|
||||
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||
@@ -198,6 +204,11 @@ def is_valid_username(username: str) -> bool:
|
||||
return re.fullmatch(username_regex, username) is not None
|
||||
|
||||
|
||||
def is_valid_label(label: str) -> bool:
|
||||
label_regex = r"([A-Za-z0-9 ._-]{1,100}$)"
|
||||
return re.fullmatch(label_regex, label) is not None
|
||||
|
||||
|
||||
def is_valid_external_id(external_id: str) -> bool:
|
||||
if len(external_id) > 256:
|
||||
return False
|
||||
@@ -353,17 +364,6 @@ def normalize_path(path: str | None) -> str:
|
||||
return "/" + "/".join(path_segments(path))
|
||||
|
||||
|
||||
def safe_upload_file_path(filename: str, directory: str = "images") -> Path:
|
||||
image_folder = Path(settings.lnbits_data_folder, directory)
|
||||
file_path = image_folder / filename
|
||||
# Prevent dir traversal attack
|
||||
if image_folder.resolve() not in file_path.resolve().parents:
|
||||
raise ValueError("Unsafe filename.")
|
||||
# Prevent filename with subdirectories
|
||||
file_path = image_folder / filename.split("/")[-1]
|
||||
return file_path.resolve()
|
||||
|
||||
|
||||
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||
if add_proto:
|
||||
@@ -397,3 +397,11 @@ def is_snake_case(v: str) -> bool:
|
||||
|
||||
def lowercase_first_letter(s: str) -> str:
|
||||
return s[:1].lower() + s[1:] if s else s
|
||||
|
||||
|
||||
def sha256s(value: str) -> str:
|
||||
"""
|
||||
SHA256 applied on a string value.
|
||||
Returns the hex as a string.
|
||||
"""
|
||||
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||
|
||||
+13
-3
@@ -280,8 +280,11 @@ class ThemesSettings(LNbitsSettings):
|
||||
class OpsSettings(LNbitsSettings):
|
||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||
lnbits_hide_api: bool = Field(default=False)
|
||||
lnbits_upload_size_bytes: int = Field(default=512_000, ge=0) # 500kb
|
||||
lnbits_upload_allowed_types: list[str] = Field(
|
||||
|
||||
|
||||
class AssetSettings(LNbitsSettings):
|
||||
lnbits_max_asset_size_mb: float = Field(default=2.5, ge=0.0)
|
||||
lnbits_assets_allowed_mime_types: list[str] = Field(
|
||||
default=[
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
@@ -297,6 +300,12 @@ class OpsSettings(LNbitsSettings):
|
||||
"heics",
|
||||
]
|
||||
)
|
||||
lnbits_asset_thumbnail_width: int = Field(default=128, ge=0)
|
||||
lnbits_asset_thumbnail_height: int = Field(default=128, ge=0)
|
||||
lnbits_asset_thumbnail_format: str = Field(default="png")
|
||||
|
||||
lnbits_max_assets_per_user: int = Field(default=1, ge=0)
|
||||
lnbits_assets_no_limit_users: list[str] = Field(default=[])
|
||||
|
||||
|
||||
class FeeSettings(LNbitsSettings):
|
||||
@@ -429,7 +438,7 @@ class NotificationsSettings(LNbitsSettings):
|
||||
|
||||
lnbits_notification_settings_update: bool = Field(default=True)
|
||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||
notification_balance_delta_changed: bool = Field(default=True)
|
||||
notification_balance_delta_threshold_sats: int = Field(default=1, ge=0)
|
||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||
lnbits_notification_watchdog: bool = Field(default=False)
|
||||
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||
@@ -867,6 +876,7 @@ class EditableSettings(
|
||||
ExtensionsSettings,
|
||||
ThemesSettings,
|
||||
OpsSettings,
|
||||
AssetSettings,
|
||||
FeeSettings,
|
||||
ExchangeProvidersSettings,
|
||||
SecuritySettings,
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+10
-10
File diff suppressed because one or more lines are too long
@@ -185,6 +185,11 @@ body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
|
||||
}
|
||||
|
||||
body.gradient-bg {
|
||||
background-image: linear-gradient(to bottom right, #fff, var(--q-primary));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
body.gradient-bg.body--dark {
|
||||
background-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
@@ -215,6 +220,53 @@ body.body--dark .q-drawer {
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--size: 100px;
|
||||
--gap: 25px;
|
||||
}
|
||||
|
||||
.home .btn-fixed-width {
|
||||
/* width: 45%; */
|
||||
}
|
||||
.home .wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
margin: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
.home .marquee {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
gap: var(--gap);
|
||||
height: max-content;
|
||||
mask-image: linear-gradient(to right, hsla(0, 0%, 0%, 0), hsl(0, 0%, 0%) 20%, hsl(0, 0%, 0%) 80%, hsla(0, 0%, 0%, 0));
|
||||
}
|
||||
.home .marquee__group {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
gap: var(--gap);
|
||||
min-width: 100%;
|
||||
animation: scroll-x 60s linear infinite;
|
||||
}
|
||||
.home .marquee:hover .marquee__group {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.home .marquee__group div {
|
||||
width: var(--size);
|
||||
}
|
||||
@keyframes scroll-x {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
@@ -341,3 +393,65 @@ video {
|
||||
.q-dialog__inner--minimized {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.first-install {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.first-install .grid {
|
||||
display: block;
|
||||
}
|
||||
.first-install .hero-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.first-install .hero {
|
||||
display: block;
|
||||
height: 100%;
|
||||
max-width: 250px;
|
||||
background-image: url(/static/images/logos/lnbits.svg);
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.first-install .grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1rem;
|
||||
}
|
||||
.first-install .hero-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.wallet-list-card {
|
||||
margin-top: 1px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.wallet-list-card:first-child {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.wallet-card {
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.mobile-simple .wallet-wrapper {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
}
|
||||
.mobile-simple .wallet-card {
|
||||
width: 90% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,14 @@ window.localisation.en = {
|
||||
reconnect: 'Reconnect',
|
||||
open_channel: 'Open Channel',
|
||||
open: 'Open',
|
||||
clear: 'Clear',
|
||||
close_channel: 'Close Channel',
|
||||
close: 'Close',
|
||||
restart: 'Restart server',
|
||||
image_library: 'Image Library',
|
||||
save: 'Save',
|
||||
save_tooltip: 'Save your changes',
|
||||
must_save: 'You have unsaved changes',
|
||||
credit_debit: 'Credit / Debit',
|
||||
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
||||
credit_label: '{denomination} to credit/debit',
|
||||
@@ -52,9 +54,17 @@ window.localisation.en = {
|
||||
stored_paylinks: 'Stored LNURL pay links',
|
||||
wallet: 'Wallet: ',
|
||||
wallet_name: 'Wallet name',
|
||||
wallet_type: 'Wallet type',
|
||||
shared_wallet: 'Shared Wallet',
|
||||
share_wallet: 'Share Wallet',
|
||||
update_permissions: 'Update Permissions',
|
||||
shared_wallet_id: 'Shared Wallet ID',
|
||||
shared_wallet_desc:
|
||||
"You have been invited to have access to someone else's wallet.",
|
||||
wallets: 'Wallets',
|
||||
exclude_wallets: 'Exclude Wallets',
|
||||
add_wallet: 'Add wallet',
|
||||
reject_wallet: 'Reject wallet',
|
||||
add_new_wallet: 'Add a new wallet',
|
||||
pin_wallet: 'Pin wallet',
|
||||
delete_wallet: 'Delete wallet',
|
||||
@@ -72,6 +82,10 @@ window.localisation.en = {
|
||||
donate: 'Donate',
|
||||
view_github: 'View on GitHub',
|
||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||
voidwallet_active_user:
|
||||
'Funding source unavailable. Please contact your admin to configure.',
|
||||
voidwallet_active_admin:
|
||||
'Funding source unavailable. Click here to configure.',
|
||||
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||
service_fee_max_badge:
|
||||
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||
@@ -137,6 +151,8 @@ window.localisation.en = {
|
||||
extensions: 'Extensions',
|
||||
no_extensions: "You don't have any extensions installed :(",
|
||||
created: 'Created',
|
||||
created_at: 'Created At',
|
||||
updated_at: 'Updated At',
|
||||
search_extensions: 'Search extensions',
|
||||
search_wallets: 'Search wallets',
|
||||
extension_sources: 'Extension Sources',
|
||||
@@ -147,6 +163,7 @@ window.localisation.en = {
|
||||
repository: 'Repository',
|
||||
confirm_continue: 'Are you sure you want to continue?',
|
||||
manage_extension_details: 'Install/uninstall extension',
|
||||
upload: 'Upload',
|
||||
install: 'Install',
|
||||
uninstall: 'Uninstall',
|
||||
drop_db: 'Remove Data',
|
||||
@@ -302,7 +319,7 @@ window.localisation.en = {
|
||||
|
||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||
notification_balance_delta_changed_desc:
|
||||
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
|
||||
'Notify when the difference between the node balance and the LNbits balance has changed by more than the specified amount (in sats). Set to 0 to disable. This runs every minute.',
|
||||
|
||||
enable_watchdog: 'Enable Watchdog Switch',
|
||||
enable_watchdog_desc:
|
||||
@@ -395,6 +412,8 @@ window.localisation.en = {
|
||||
first_name: 'First Name',
|
||||
last_name: 'Last Name',
|
||||
picture: 'Picture',
|
||||
user_picture_desc:
|
||||
'URL to an image to use as profile picture. You can upload it as an asset.',
|
||||
verify_email: 'Verify email with',
|
||||
account: 'Account',
|
||||
update_account: 'Update Account',
|
||||
@@ -419,6 +438,26 @@ window.localisation.en = {
|
||||
toggle_gradient: 'Toggle Gradient',
|
||||
gradient_background: 'Gradient Background',
|
||||
language: 'Language',
|
||||
assets: 'Assets',
|
||||
max_asset_size_mb: 'Max Asset Size (MB)',
|
||||
max_asset_size_mb_desc:
|
||||
'The maximum allowed size for asset uploads in megabytes (can use decimal values).',
|
||||
assets_allowed_mime_types: 'Allowed MIME Types',
|
||||
assets_allowed_mime_types_desc:
|
||||
'The MIME types that are allowed for asset uploads. No value means all uploads are allowed.',
|
||||
thumbnail_width: 'Thumbnail Width',
|
||||
thumbnail_width_desc: 'Width of the generated thumbnail in pixels.',
|
||||
thumbnail_height: 'Thumbnail Height',
|
||||
thumbnail_height_desc: 'Height of the generated thumbnail in pixels.',
|
||||
thumbnail_format: 'Thumbnail Format',
|
||||
thumbnail_format_desc:
|
||||
'Image format of the generated thumbnail (PNG, JPEG, etc.).',
|
||||
max_assets_per_user: 'Max Assets Per User',
|
||||
max_assets_per_user_desc:
|
||||
'The maximum number of assets a user can upload. Zero means upload forbidden.',
|
||||
assets_no_limit_users: 'Users Without Asset Limits',
|
||||
assets_no_limit_users_desc:
|
||||
'These users can upload an unlimited number of assets (user id based).',
|
||||
color_scheme: 'Color Scheme',
|
||||
visible_wallet_count: 'Visible Wallet Count',
|
||||
admin_settings: 'Admin Settings',
|
||||
@@ -526,7 +565,9 @@ window.localisation.en = {
|
||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||
payment_wait_time: 'Payment Wait Time (sec)',
|
||||
payment_wait_time_desc:
|
||||
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
||||
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
||||
payment_wait_time_tooltip:
|
||||
'Controls how long LNbits waits for an outgoing payment attempt to confirm before marking it as pending. Higher values help when paying slow-settling invoices (e.g., HODL invoices, Boltz). The payment will be rechecked later and updated automatically or manually.',
|
||||
server_management: 'Server Management',
|
||||
base_url: 'Base URL',
|
||||
base_url_label: 'Static/Base url for the server',
|
||||
@@ -655,6 +696,7 @@ window.localisation.en = {
|
||||
view_list: 'View wallets as list',
|
||||
view_column: 'View wallets as rows',
|
||||
filter_payments: 'Filter payments',
|
||||
filter_labels: 'Filter labels',
|
||||
filter_date: 'Filter by date',
|
||||
websocket_example: 'Websocket example',
|
||||
secret_key: 'Secret Key',
|
||||
@@ -670,5 +712,18 @@ window.localisation.en = {
|
||||
paid: 'Paid',
|
||||
funding_source_retries: 'Max Retries',
|
||||
funding_source_retries_desc:
|
||||
'Maximum number of retries for funding sources, before it falls back to VoidWallet.'
|
||||
'Maximum number of retries for funding sources, before it falls back to VoidWallet.',
|
||||
add_label: 'Add Label',
|
||||
label: 'Label',
|
||||
labels: 'Labels',
|
||||
label_filter: 'Label Filter',
|
||||
no_labels_defined: 'No labels defined yet',
|
||||
manage_labels: 'Manage Labels',
|
||||
update_label: 'Update Label',
|
||||
delete_label: 'Delete Label',
|
||||
add_remove_labels: 'Add or Remove Labels',
|
||||
payment_labels_updated: 'Payment labels updated',
|
||||
color: 'Color',
|
||||
sort: 'Sort',
|
||||
sort_by: 'Sort by'
|
||||
}
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
window.AdminPageLogic = {
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
logs: [],
|
||||
library_images: [],
|
||||
serverlogEnabled: false,
|
||||
lnbits_theme_options: [
|
||||
'classic',
|
||||
'bitcoin',
|
||||
'flamingo',
|
||||
'cyber',
|
||||
'freedom',
|
||||
'mint',
|
||||
'autumn',
|
||||
'monochrome',
|
||||
'salvador'
|
||||
],
|
||||
reactionOptions: [
|
||||
'none',
|
||||
'confettiBothSides',
|
||||
'confettiFireworks',
|
||||
'confettiStars',
|
||||
'confettiTop'
|
||||
],
|
||||
globalBorderOptions: [
|
||||
'retro-border',
|
||||
'hard-border',
|
||||
'neon-border',
|
||||
'no-border'
|
||||
],
|
||||
auditData: {},
|
||||
statusData: {},
|
||||
statusDataTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: this.$t('date'),
|
||||
field: 'date'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'message'
|
||||
}
|
||||
]
|
||||
},
|
||||
formData: {
|
||||
lnbits_exchange_rate_providers: []
|
||||
},
|
||||
chartReady: false,
|
||||
formAddAdmin: '',
|
||||
formAddUser: '',
|
||||
formAddStripeUser: '',
|
||||
hideInputToggle: true,
|
||||
formAddExtensionsManifest: '',
|
||||
nostrNotificationIdentifier: '',
|
||||
emailNotificationAddress: '',
|
||||
formAllowedIPs: '',
|
||||
formCallbackUrlRule: '',
|
||||
formBlockedIPs: '',
|
||||
nostrAcceptedUrl: '',
|
||||
formAddIncludePath: '',
|
||||
formAddExcludePath: '',
|
||||
formAddIncludeResponseCode: '',
|
||||
isSuperUser: false,
|
||||
wallet: {},
|
||||
cancel: {},
|
||||
colors: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'accent',
|
||||
'positive',
|
||||
'negative',
|
||||
'info',
|
||||
'warning',
|
||||
'red',
|
||||
'yellow',
|
||||
'orange'
|
||||
],
|
||||
tab: 'funding',
|
||||
needsRestart: false,
|
||||
exchangesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Exchange Name',
|
||||
field: 'name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'api_url',
|
||||
align: 'left',
|
||||
label: 'URL',
|
||||
field: 'api_url',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
align: 'left',
|
||||
label: 'JSON Path',
|
||||
field: 'path',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
name: 'exclude_to',
|
||||
align: 'left',
|
||||
label: 'Exclude Currencies',
|
||||
field: 'exclude_to',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'ticker_conversion',
|
||||
align: 'left',
|
||||
label: 'Ticker Conversion',
|
||||
field: 'ticker_conversion',
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 100,
|
||||
page: 1,
|
||||
rowsNumber: 100
|
||||
},
|
||||
search: null,
|
||||
hideEmpty: true
|
||||
},
|
||||
exchangeData: {
|
||||
selectedProvider: null,
|
||||
showTickerConversion: false,
|
||||
convertFromTicker: null,
|
||||
convertToTicker: null
|
||||
}
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.getSettings()
|
||||
await this.getAudit()
|
||||
await this.getUploadedImages()
|
||||
this.balance = +'{{ balance|safe }}'
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
if (hash === 'exchange_providers') {
|
||||
this.showExchangeProvidersTab(hash)
|
||||
}
|
||||
if (hash) {
|
||||
this.tab = hash
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lnbitsVersion() {
|
||||
return LNBITS_VERSION
|
||||
},
|
||||
checkChanges() {
|
||||
return !_.isEqual(this.settings, this.formData)
|
||||
},
|
||||
updateAvailable() {
|
||||
return LNBITS_VERSION !== this.statusData.version
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAdminUser() {
|
||||
let addUser = this.formAddAdmin
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
if (addUser && addUser.length && !admin_users.includes(addUser)) {
|
||||
this.formData.lnbits_admin_users = [...admin_users, addUser]
|
||||
this.formAddAdmin = ''
|
||||
}
|
||||
},
|
||||
removeAdminUser(user) {
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||
},
|
||||
addAllowedUser() {
|
||||
let addUser = this.formAddUser
|
||||
let allowed_users = this.formData.lnbits_allowed_users
|
||||
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
|
||||
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
|
||||
this.formAddUser = ''
|
||||
}
|
||||
},
|
||||
removeAllowedUser(user) {
|
||||
let allowed_users = this.formData.lnbits_allowed_users
|
||||
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
||||
},
|
||||
addStripeAllowedUser() {
|
||||
const addUser = this.formAddStripeUser || ''
|
||||
if (
|
||||
addUser.length &&
|
||||
!this.formData.stripe_limits.allowed_users.includes(addUser)
|
||||
) {
|
||||
this.formData.stripe_limits.allowed_users = [
|
||||
...this.formData.stripe_limits.allowed_users,
|
||||
addUser
|
||||
]
|
||||
this.formAddStripeUser = ''
|
||||
}
|
||||
},
|
||||
removeStripeAllowedUser(user) {
|
||||
this.formData.stripe_limits.allowed_users =
|
||||
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
||||
},
|
||||
addIncludePath() {
|
||||
if (!this.formAddIncludePath) {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_include_paths
|
||||
if (!paths.includes(this.formAddIncludePath)) {
|
||||
this.formData.lnbits_audit_include_paths = [
|
||||
...paths,
|
||||
this.formAddIncludePath
|
||||
]
|
||||
}
|
||||
this.formAddIncludePath = ''
|
||||
},
|
||||
removeIncludePath(path) {
|
||||
this.formData.lnbits_audit_include_paths =
|
||||
this.formData.lnbits_audit_include_paths.filter(p => p !== path)
|
||||
},
|
||||
addExcludePath() {
|
||||
if (!this.formAddExcludePath) {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_exclude_paths
|
||||
if (!paths.includes(this.formAddExcludePath)) {
|
||||
this.formData.lnbits_audit_exclude_paths = [
|
||||
...paths,
|
||||
this.formAddExcludePath
|
||||
]
|
||||
}
|
||||
this.formAddExcludePath = ''
|
||||
},
|
||||
|
||||
removeExcludePath(path) {
|
||||
this.formData.lnbits_audit_exclude_paths =
|
||||
this.formData.lnbits_audit_exclude_paths.filter(p => p !== path)
|
||||
},
|
||||
addIncludeResponseCode() {
|
||||
if (!this.formAddIncludeResponseCode) {
|
||||
return
|
||||
}
|
||||
const codes = this.formData.lnbits_audit_http_response_codes
|
||||
if (!codes.includes(this.formAddIncludeResponseCode)) {
|
||||
this.formData.lnbits_audit_http_response_codes = [
|
||||
...codes,
|
||||
this.formAddIncludeResponseCode
|
||||
]
|
||||
}
|
||||
this.formAddIncludeResponseCode = ''
|
||||
},
|
||||
removeIncludeResponseCode(code) {
|
||||
this.formData.lnbits_audit_http_response_codes =
|
||||
this.formData.lnbits_audit_http_response_codes.filter(c => c !== code)
|
||||
},
|
||||
addExtensionsManifest() {
|
||||
const addManifest = this.formAddExtensionsManifest.trim()
|
||||
const manifests = this.formData.lnbits_extensions_manifests
|
||||
if (
|
||||
addManifest &&
|
||||
addManifest.length &&
|
||||
!manifests.includes(addManifest)
|
||||
) {
|
||||
this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
|
||||
this.formAddExtensionsManifest = ''
|
||||
}
|
||||
},
|
||||
removeExtensionsManifest(manifest) {
|
||||
const manifests = this.formData.lnbits_extensions_manifests
|
||||
this.formData.lnbits_extensions_manifests = manifests.filter(
|
||||
m => m !== manifest
|
||||
)
|
||||
},
|
||||
addNostrNotificationIdentifier() {
|
||||
const identifer = this.nostrNotificationIdentifier.trim()
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
if (identifer && identifer.length && !identifiers.includes(identifer)) {
|
||||
this.formData.lnbits_nostr_notifications_identifiers = [
|
||||
...identifiers,
|
||||
identifer
|
||||
]
|
||||
this.nostrNotificationIdentifier = ''
|
||||
}
|
||||
},
|
||||
removeNostrNotificationIdentifier(identifer) {
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
|
||||
m => m !== identifer
|
||||
)
|
||||
},
|
||||
addEmailNotificationAddress() {
|
||||
const email = this.emailNotificationAddress.trim()
|
||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||
if (email && email.length && !emails.includes(email)) {
|
||||
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
|
||||
this.emailNotificationAddress = ''
|
||||
}
|
||||
},
|
||||
removeEmailNotificationAddress(email) {
|
||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||
this.formData.lnbits_email_notifications_to_emails = emails.filter(
|
||||
m => m !== email
|
||||
)
|
||||
},
|
||||
hideInputsToggle() {
|
||||
this.hideInputToggle = !this.hideInputToggle
|
||||
},
|
||||
async toggleServerLog() {
|
||||
this.serverlogEnabled = !this.serverlogEnabled
|
||||
if (this.serverlogEnabled) {
|
||||
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
|
||||
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
|
||||
const localUrl =
|
||||
wsProto +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/api/v1/ws/' +
|
||||
digestHex
|
||||
this.ws = new WebSocket(localUrl)
|
||||
this.ws.addEventListener('message', async ({data}) => {
|
||||
this.logs.push(data.toString())
|
||||
const scrollArea = this.$refs.logScroll
|
||||
if (scrollArea) {
|
||||
const scrollTarget = scrollArea.getScrollTarget()
|
||||
const duration = 0
|
||||
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.ws.close()
|
||||
}
|
||||
},
|
||||
addAllowedIPs() {
|
||||
const allowedIPs = this.formAllowedIPs.trim()
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
if (
|
||||
allowedIPs &&
|
||||
allowedIPs.length &&
|
||||
!allowed_ips.includes(allowedIPs)
|
||||
) {
|
||||
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
|
||||
this.formAllowedIPs = ''
|
||||
}
|
||||
},
|
||||
removeAllowedIPs(allowed_ip) {
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
this.formData.lnbits_allowed_ips = allowed_ips.filter(
|
||||
a => a !== allowed_ip
|
||||
)
|
||||
},
|
||||
addBlockedIPs() {
|
||||
const blockedIPs = this.formBlockedIPs.trim()
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
if (
|
||||
blockedIPs &&
|
||||
blockedIPs.length &&
|
||||
!blocked_ips.includes(blockedIPs)
|
||||
) {
|
||||
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
|
||||
this.formBlockedIPs = ''
|
||||
}
|
||||
},
|
||||
removeBlockedIPs(blocked_ip) {
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
this.formData.lnbits_blocked_ips = blocked_ips.filter(
|
||||
b => b !== blocked_ip
|
||||
)
|
||||
},
|
||||
addCallbackUrlRule() {
|
||||
const allowedCallback = this.formCallbackUrlRule.trim()
|
||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
||||
if (
|
||||
allowedCallback &&
|
||||
allowedCallback.length &&
|
||||
!allowedCallbacks.includes(allowedCallback)
|
||||
) {
|
||||
this.formData.lnbits_callback_url_rules = [
|
||||
...allowedCallbacks,
|
||||
allowedCallback
|
||||
]
|
||||
this.formCallbackUrlRule = ''
|
||||
}
|
||||
},
|
||||
removeCallbackUrlRule(allowedCallback) {
|
||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
||||
this.formData.lnbits_callback_url_rules = allowedCallbacks.filter(
|
||||
a => a !== allowedCallback
|
||||
)
|
||||
},
|
||||
|
||||
addNostrUrl() {
|
||||
const url = this.nostrAcceptedUrl.trim()
|
||||
this.removeNostrUrl(url)
|
||||
this.formData.nostr_absolute_request_urls.push(url)
|
||||
this.nostrAcceptedUrl = ''
|
||||
},
|
||||
removeNostrUrl(url) {
|
||||
this.formData.nostr_absolute_request_urls =
|
||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
||||
},
|
||||
addExchangeProvider() {
|
||||
this.formData.lnbits_exchange_rate_providers = [
|
||||
{
|
||||
name: '',
|
||||
api_url: '',
|
||||
path: '',
|
||||
exclude_to: []
|
||||
},
|
||||
...this.formData.lnbits_exchange_rate_providers
|
||||
]
|
||||
},
|
||||
removeExchangeProvider(provider) {
|
||||
this.formData.lnbits_exchange_rate_providers =
|
||||
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
||||
},
|
||||
removeExchangeTickerConversion(provider, ticker) {
|
||||
provider.ticker_conversion = provider.ticker_conversion.filter(
|
||||
t => t !== ticker
|
||||
)
|
||||
this.touchSettings()
|
||||
},
|
||||
addExchangeTickerConversion() {
|
||||
if (!this.exchangeData.selectedProvider) {
|
||||
return
|
||||
}
|
||||
this.exchangeData.selectedProvider.ticker_conversion.push(
|
||||
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
||||
)
|
||||
this.touchSettings()
|
||||
this.exchangeData.showTickerConversion = false
|
||||
},
|
||||
showTickerConversionDialog(provider) {
|
||||
this.exchangeData.convertFromTicker = null
|
||||
this.exchangeData.convertToTicker = null
|
||||
this.exchangeData.selectedProvider = provider
|
||||
this.exchangeData.showTickerConversion = true
|
||||
},
|
||||
|
||||
getDefaultSetting(fieldName) {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
`/admin/api/v1/settings/default?field_name=${fieldName}`
|
||||
)
|
||||
.then(response => {
|
||||
this.formData[fieldName] = response.data.default_value
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
restartServer() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/restart/')
|
||||
.then(response => {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Success! Restarted Server',
|
||||
icon: null
|
||||
})
|
||||
this.needsRestart = false
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
formatDate(date) {
|
||||
return moment
|
||||
.utc(date * 1000)
|
||||
.local()
|
||||
.fromNow()
|
||||
},
|
||||
sendTestEmail() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/admin/api/v1/testemail',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
if (response.data.status === 'error') {
|
||||
throw new Error(response.data.message)
|
||||
}
|
||||
this.$q.notify({
|
||||
message: 'Test email sent!',
|
||||
color: 'positive'
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify({
|
||||
message: error.message,
|
||||
color: 'negative'
|
||||
})
|
||||
})
|
||||
},
|
||||
getAudit() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||
.then(response => {
|
||||
this.auditData = response.data
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
getExchangeRateHistory() {
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||
.then(response => {
|
||||
this.initExchangeChart(response.data)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
async getSettings() {
|
||||
await LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/admin/api/v1/settings',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.isSuperUser = response.data.is_super_user || false
|
||||
this.settings = response.data
|
||||
this.formData = {...this.settings}
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
updateSettings() {
|
||||
const data = _.omit(this.formData, [
|
||||
'is_super_user',
|
||||
'lnbits_allowed_funding_sources',
|
||||
'touch'
|
||||
])
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/admin/api/v1/settings',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
data
|
||||
)
|
||||
.then(response => {
|
||||
this.needsRestart =
|
||||
this.settings.lnbits_backend_wallet_class !==
|
||||
this.formData.lnbits_backend_wallet_class
|
||||
this.settings = this.formData
|
||||
this.formData = _.clone(this.settings)
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: `Success! Settings changed! ${
|
||||
this.needsRestart ? 'Restart required!' : ''
|
||||
}`,
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
deleteSettings() {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to restore settings to default?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.request('DELETE', '/admin/api/v1/settings')
|
||||
.then(response => {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message:
|
||||
'Success! Restored settings to defaults. Restarting...',
|
||||
icon: null
|
||||
})
|
||||
this.$q.localStorage.clear()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
onImageInput(e) {
|
||||
const file = e.target.files[0]
|
||||
if (file) {
|
||||
this.uploadImage(file)
|
||||
}
|
||||
},
|
||||
uploadImage(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/admin/api/v1/images',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
formData,
|
||||
{headers: {'Content-Type': 'multipart/form-data'}}
|
||||
)
|
||||
.then(() => {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Image uploaded!',
|
||||
icon: null
|
||||
})
|
||||
this.getUploadedImages()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
getUploadedImages() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/images', this.g.user.wallets[0].inkey)
|
||||
.then(response => {
|
||||
this.library_images = response.data.map(image => ({
|
||||
...image,
|
||||
url: `${window.origin}/${image.directory}/${image.filename}`
|
||||
}))
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
deleteImage(filename) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this image?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
`/admin/api/v1/images/${filename}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(() => {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Image deleted!',
|
||||
icon: null
|
||||
})
|
||||
this.getUploadedImages()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
checkFiatProvider(providerName) {
|
||||
LNbits.api
|
||||
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
||||
.then(response => {
|
||||
response
|
||||
const data = response.data
|
||||
Quasar.Notify.create({
|
||||
type: data.success ? 'positive' : 'warning',
|
||||
message: data.message,
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
downloadBackup() {
|
||||
window.open('/admin/api/v1/backup', '_blank')
|
||||
},
|
||||
showExchangeProvidersTab(tabName) {
|
||||
if (tabName === 'exchange_providers') {
|
||||
this.getExchangeRateHistory()
|
||||
}
|
||||
},
|
||||
touchSettings() {
|
||||
this.formData.touch = null
|
||||
},
|
||||
initExchangeChart(data) {
|
||||
const xValues = data.map(d =>
|
||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||
)
|
||||
const exchanges = [
|
||||
...this.formData.lnbits_exchange_rate_providers,
|
||||
{name: 'LNbits'}
|
||||
]
|
||||
const datasets = exchanges.map(exchange => ({
|
||||
label: exchange.name,
|
||||
data: data.map(d => d.rates[exchange.name]),
|
||||
pointStyle: true,
|
||||
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
||||
tension: 0.4
|
||||
}))
|
||||
this.exchangeRatesChart = new Chart(
|
||||
this.$refs.exchangeRatesChart.getContext('2d'),
|
||||
{
|
||||
type: 'line',
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
labels: xValues,
|
||||
datasets
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
window._lnbitsApi = {
|
||||
request(method, url, apiKey, data, options = {}) {
|
||||
return axios({
|
||||
method: method,
|
||||
url: url,
|
||||
headers: {
|
||||
'X-Api-Key': apiKey
|
||||
},
|
||||
data: data,
|
||||
...options
|
||||
})
|
||||
},
|
||||
getServerHealth() {
|
||||
return this.request('get', '/api/v1/health')
|
||||
},
|
||||
async createInvoice(
|
||||
wallet,
|
||||
amount,
|
||||
memo,
|
||||
unit = 'sat',
|
||||
lnurlWithdraw = null,
|
||||
fiatProvider = null,
|
||||
internalMemo = null,
|
||||
payment_hash = null
|
||||
) {
|
||||
const data = {
|
||||
out: false,
|
||||
amount: amount,
|
||||
memo: memo,
|
||||
unit: unit,
|
||||
lnurl_withdraw: lnurlWithdraw,
|
||||
fiat_provider: fiatProvider,
|
||||
payment_hash: payment_hash
|
||||
}
|
||||
if (internalMemo) {
|
||||
data.extra = {
|
||||
internal_memo: String(internalMemo)
|
||||
}
|
||||
}
|
||||
return this.request('post', '/api/v1/payments', wallet.inkey, data)
|
||||
},
|
||||
payInvoice(wallet, bolt11, internalMemo = null) {
|
||||
const data = {
|
||||
out: true,
|
||||
bolt11: bolt11
|
||||
}
|
||||
if (internalMemo) {
|
||||
data.extra = {
|
||||
internal_memo: String(internalMemo)
|
||||
}
|
||||
}
|
||||
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
||||
},
|
||||
cancelInvoice(wallet, paymentHash) {
|
||||
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
||||
payment_hash: paymentHash
|
||||
})
|
||||
},
|
||||
settleInvoice(wallet, preimage) {
|
||||
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
|
||||
preimage: preimage
|
||||
})
|
||||
},
|
||||
createAccount(name) {
|
||||
return this.request('post', '/api/v1/account', null, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
register(username, email, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/register',
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
password_repeat
|
||||
}
|
||||
})
|
||||
},
|
||||
reset(reset_key, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'PUT',
|
||||
url: '/api/v1/auth/reset',
|
||||
data: {
|
||||
reset_key,
|
||||
password,
|
||||
password_repeat
|
||||
}
|
||||
})
|
||||
},
|
||||
login(username, password) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth',
|
||||
data: {username, password}
|
||||
})
|
||||
},
|
||||
loginByProvider(provider, headers, data) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: `/api/v1/auth/${provider}`,
|
||||
headers: headers,
|
||||
data
|
||||
})
|
||||
},
|
||||
loginUsr(usr) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/usr',
|
||||
data: {usr}
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/logout'
|
||||
})
|
||||
},
|
||||
getAuthenticatedUser() {
|
||||
return this.request('get', '/api/v1/auth')
|
||||
},
|
||||
getWallet(wallet) {
|
||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||
},
|
||||
createWallet(wallet, name, walletType, ops = {}) {
|
||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name,
|
||||
wallet_type: walletType,
|
||||
...ops
|
||||
}).then(res => {
|
||||
window.location = '/wallet?wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
resetWalletKeys(wallet) {
|
||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||
res => {
|
||||
return res.data
|
||||
}
|
||||
)
|
||||
},
|
||||
deleteWallet(wallet) {
|
||||
return this.request('delete', `/api/v1/wallet/${wallet.id}`).then(_ => {
|
||||
let url = new URL(window.location.href)
|
||||
url.searchParams.delete('wal')
|
||||
window.location = url
|
||||
})
|
||||
},
|
||||
getPayments(wallet, params) {
|
||||
return this.request(
|
||||
'get',
|
||||
'/api/v1/payments/paginated?' + params,
|
||||
wallet.inkey
|
||||
)
|
||||
},
|
||||
getPayment(wallet, paymentHash) {
|
||||
return this.request('get', '/api/v1/payments/' + paymentHash, wallet.inkey)
|
||||
},
|
||||
updateBalance(credit, wallet_id) {
|
||||
return this.request('PUT', '/users/api/v1/balance', null, {
|
||||
amount: credit,
|
||||
id: wallet_id
|
||||
})
|
||||
},
|
||||
getCurrencies() {
|
||||
return this.request('GET', '/api/v1/currencies').then(response => {
|
||||
return ['sats', ...response.data]
|
||||
})
|
||||
}
|
||||
}
|
||||
+22
-787
@@ -1,200 +1,8 @@
|
||||
window.LNbits = {
|
||||
g: window.g,
|
||||
api: {
|
||||
request(method, url, apiKey, data, options = {}) {
|
||||
return axios({
|
||||
method: method,
|
||||
url: url,
|
||||
headers: {
|
||||
'X-Api-Key': apiKey
|
||||
},
|
||||
data: data,
|
||||
...options
|
||||
})
|
||||
},
|
||||
getServerHealth() {
|
||||
return this.request('get', '/api/v1/health')
|
||||
},
|
||||
async createInvoice(
|
||||
wallet,
|
||||
amount,
|
||||
memo,
|
||||
unit = 'sat',
|
||||
lnurlWithdraw = null,
|
||||
fiatProvider = null,
|
||||
internalMemo = null,
|
||||
payment_hash = null
|
||||
) {
|
||||
const data = {
|
||||
out: false,
|
||||
amount: amount,
|
||||
memo: memo,
|
||||
unit: unit,
|
||||
lnurl_withdraw: lnurlWithdraw,
|
||||
fiat_provider: fiatProvider,
|
||||
payment_hash: payment_hash
|
||||
}
|
||||
if (internalMemo) {
|
||||
data.extra = {
|
||||
internal_memo: String(internalMemo)
|
||||
}
|
||||
}
|
||||
return this.request('post', '/api/v1/payments', wallet.inkey, data)
|
||||
},
|
||||
payInvoice(wallet, bolt11, internalMemo = null) {
|
||||
const data = {
|
||||
out: true,
|
||||
bolt11: bolt11
|
||||
}
|
||||
if (internalMemo) {
|
||||
data.extra = {
|
||||
internal_memo: String(internalMemo)
|
||||
}
|
||||
}
|
||||
return this.request('post', '/api/v1/payments', wallet.adminkey, data)
|
||||
},
|
||||
cancelInvoice(wallet, paymentHash) {
|
||||
return this.request('post', '/api/v1/payments/cancel', wallet.adminkey, {
|
||||
payment_hash: paymentHash
|
||||
})
|
||||
},
|
||||
settleInvoice(wallet, preimage) {
|
||||
return this.request('post', `/api/v1/payments/settle`, wallet.adminkey, {
|
||||
preimage: preimage
|
||||
})
|
||||
},
|
||||
createAccount(name) {
|
||||
return this.request('post', '/api/v1/account', null, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
register(username, email, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/register',
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
password_repeat
|
||||
}
|
||||
})
|
||||
},
|
||||
reset(reset_key, password, password_repeat) {
|
||||
return axios({
|
||||
method: 'PUT',
|
||||
url: '/api/v1/auth/reset',
|
||||
data: {
|
||||
reset_key,
|
||||
password,
|
||||
password_repeat
|
||||
}
|
||||
})
|
||||
},
|
||||
login(username, password) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth',
|
||||
data: {username, password}
|
||||
})
|
||||
},
|
||||
loginByProvider(provider, headers, data) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: `/api/v1/auth/${provider}`,
|
||||
headers: headers,
|
||||
data
|
||||
})
|
||||
},
|
||||
loginUsr(usr) {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/usr',
|
||||
data: {usr}
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
return axios({
|
||||
method: 'POST',
|
||||
url: '/api/v1/auth/logout'
|
||||
})
|
||||
},
|
||||
getAuthenticatedUser() {
|
||||
return this.request('get', '/api/v1/auth')
|
||||
},
|
||||
getWallet(wallet) {
|
||||
return this.request('get', '/api/v1/wallet', wallet.inkey)
|
||||
},
|
||||
createWallet(wallet, name) {
|
||||
return this.request('post', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
}).then(res => {
|
||||
window.location = '/wallet?wal=' + res.data.id
|
||||
})
|
||||
},
|
||||
updateWallet(name, wallet) {
|
||||
return this.request('patch', '/api/v1/wallet', wallet.adminkey, {
|
||||
name: name
|
||||
})
|
||||
},
|
||||
resetWalletKeys(wallet) {
|
||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||
res => {
|
||||
return res.data
|
||||
}
|
||||
)
|
||||
},
|
||||
deleteWallet(wallet) {
|
||||
return this.request('delete', `/api/v1/wallet/${wallet.id}`).then(_ => {
|
||||
let url = new URL(window.location.href)
|
||||
url.searchParams.delete('wal')
|
||||
window.location = url
|
||||
})
|
||||
},
|
||||
getPayments(wallet, params) {
|
||||
return this.request(
|
||||
'get',
|
||||
'/api/v1/payments/paginated?' + params,
|
||||
wallet.inkey
|
||||
)
|
||||
},
|
||||
getPayment(wallet, paymentHash) {
|
||||
return this.request(
|
||||
'get',
|
||||
'/api/v1/payments/' + paymentHash,
|
||||
wallet.inkey
|
||||
)
|
||||
},
|
||||
updateBalance(credit, wallet_id) {
|
||||
return this.request('PUT', '/users/api/v1/balance', null, {
|
||||
amount: credit,
|
||||
id: wallet_id
|
||||
})
|
||||
},
|
||||
getCurrencies() {
|
||||
return this.request('GET', '/api/v1/currencies').then(response => {
|
||||
return ['sats', ...response.data]
|
||||
})
|
||||
}
|
||||
},
|
||||
events: {
|
||||
onInvoicePaid(wallet, cb) {
|
||||
ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`)
|
||||
ws.onmessage = ev => {
|
||||
const data = JSON.parse(ev.data)
|
||||
if (data.payment) {
|
||||
cb(data)
|
||||
}
|
||||
}
|
||||
return ws.onclose
|
||||
}
|
||||
},
|
||||
utils: window._lnbitsUtils,
|
||||
api: window._lnbitsApi,
|
||||
map: {
|
||||
extension(data) {
|
||||
const obj = {...data}
|
||||
obj.url = ['/', obj.code, '/'].join('')
|
||||
return obj
|
||||
},
|
||||
user(data) {
|
||||
const obj = {
|
||||
id: data.id,
|
||||
@@ -207,16 +15,12 @@ window.LNbits = {
|
||||
extra: data.extra ?? {}
|
||||
}
|
||||
const mapWallet = this.wallet
|
||||
obj.wallets = obj.wallets
|
||||
.map(obj => {
|
||||
return mapWallet(obj)
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||
if (a.extra.pinned !== b.extra.pinned) {
|
||||
return a.extra.pinned ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
obj.walletOptions = obj.wallets.map(obj => {
|
||||
return {
|
||||
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||
@@ -227,605 +31,36 @@ window.LNbits = {
|
||||
0,
|
||||
data.wallets.length - data.extra.visible_wallet_count
|
||||
)
|
||||
obj.walletInvitesCount = data.extra.wallet_invite_requests?.length || 0
|
||||
return obj
|
||||
},
|
||||
wallet(data) {
|
||||
newWallet = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
walletType: data.wallet_type,
|
||||
sharePermissions: data.share_permissions,
|
||||
sharedWalletId: data.shared_wallet_id,
|
||||
adminkey: data.adminkey,
|
||||
inkey: data.inkey,
|
||||
currency: data.currency,
|
||||
extra: data.extra
|
||||
extra: data.extra,
|
||||
canReceivePayments: true,
|
||||
canSendPayments: true
|
||||
}
|
||||
newWallet.msat = data.balance_msat
|
||||
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
||||
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
||||
newWallet.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
||||
newWallet.sat
|
||||
)
|
||||
if (newWallet.walletType === 'lightning-shared') {
|
||||
const perms = newWallet.sharePermissions
|
||||
newWallet.canReceivePayments = perms.includes('receive-payments')
|
||||
newWallet.canSendPayments = perms.includes('send-payments')
|
||||
}
|
||||
newWallet.url = `/wallet?&wal=${data.id}`
|
||||
newWallet.storedPaylinks = data.stored_paylinks.links
|
||||
return newWallet
|
||||
},
|
||||
payment(data) {
|
||||
obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency
|
||||
}
|
||||
|
||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||
|
||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = new Intl.NumberFormat(window.LOCALE).format(obj.sat)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
},
|
||||
utils: {
|
||||
confirmDialog(msg) {
|
||||
return Quasar.Dialog.create({
|
||||
message: msg,
|
||||
ok: {
|
||||
flat: true,
|
||||
color: 'orange'
|
||||
},
|
||||
cancel: {
|
||||
flat: true,
|
||||
color: 'grey'
|
||||
}
|
||||
})
|
||||
},
|
||||
async digestMessage(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
return hashHex
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
return Quasar.date.formatDate(
|
||||
new Date(timestamp * 1000),
|
||||
window.dateFormat
|
||||
)
|
||||
},
|
||||
formatDateString(isoDateString) {
|
||||
return Quasar.date.formatDate(new Date(isoDateString), window.dateFormat)
|
||||
},
|
||||
formatCurrency(value, currency) {
|
||||
return new Intl.NumberFormat(window.LOCALE, {
|
||||
style: 'currency',
|
||||
currency: currency || 'sat'
|
||||
}).format(value)
|
||||
},
|
||||
formatSat(value) {
|
||||
return new Intl.NumberFormat(window.LOCALE).format(value)
|
||||
},
|
||||
formatMsat(value) {
|
||||
return this.formatSat(value / 1000)
|
||||
},
|
||||
notifyApiError(error) {
|
||||
if (!error.response) {
|
||||
return console.error(error)
|
||||
}
|
||||
const types = {
|
||||
400: 'warning',
|
||||
401: 'warning',
|
||||
500: 'negative'
|
||||
}
|
||||
Quasar.Notify.create({
|
||||
timeout: 5000,
|
||||
type: types[error.response.status] || 'warning',
|
||||
message:
|
||||
error.response.data.message || error.response.data.detail || null,
|
||||
caption:
|
||||
[error.response.status, ' ', error.response.statusText]
|
||||
.join('')
|
||||
.toUpperCase() || null,
|
||||
icon: null
|
||||
})
|
||||
},
|
||||
search(data, q, field, separator) {
|
||||
try {
|
||||
const queries = q.toLowerCase().split(separator || ' ')
|
||||
return data.filter(obj => {
|
||||
let matches = 0
|
||||
_.each(queries, q => {
|
||||
if (obj[field].indexOf(q) !== -1) matches++
|
||||
})
|
||||
return matches === queries.length
|
||||
})
|
||||
} catch (err) {
|
||||
return data
|
||||
}
|
||||
},
|
||||
prepareFilterQuery(tableConfig, props) {
|
||||
tableConfig.filter = tableConfig.filter || {}
|
||||
if (props) {
|
||||
tableConfig.pagination = props.pagination
|
||||
Object.assign(tableConfig.filter, props.filter)
|
||||
}
|
||||
const pagination = tableConfig.pagination
|
||||
tableConfig.loading = true
|
||||
const query = {
|
||||
limit: pagination.rowsPerPage,
|
||||
offset: (pagination.page - 1) * pagination.rowsPerPage,
|
||||
sortby: pagination.sortBy ?? '',
|
||||
direction: pagination.descending ? 'desc' : 'asc',
|
||||
...tableConfig.filter
|
||||
}
|
||||
if (tableConfig.search) {
|
||||
query.search = tableConfig.search
|
||||
}
|
||||
return new URLSearchParams(query)
|
||||
},
|
||||
exportCSV(columns, data, fileName) {
|
||||
const wrapCsvValue = (val, formatFn) => {
|
||||
let formatted = formatFn !== void 0 ? formatFn(val) : val
|
||||
|
||||
formatted =
|
||||
formatted === void 0 || formatted === null ? '' : String(formatted)
|
||||
|
||||
formatted = formatted.split('"').join('""')
|
||||
|
||||
return `"${formatted}"`
|
||||
}
|
||||
|
||||
const content = [
|
||||
columns.map(col => {
|
||||
return wrapCsvValue(col.label)
|
||||
})
|
||||
]
|
||||
.concat(
|
||||
data.map(row => {
|
||||
return columns
|
||||
.map(col => {
|
||||
return wrapCsvValue(
|
||||
typeof col.field === 'function'
|
||||
? col.field(row)
|
||||
: row[col.field === void 0 ? col.name : col.field],
|
||||
col.format
|
||||
)
|
||||
})
|
||||
.join(',')
|
||||
})
|
||||
)
|
||||
.join('\r\n')
|
||||
|
||||
const status = Quasar.exportFile(
|
||||
`${fileName || 'table-export'}.csv`,
|
||||
content,
|
||||
'text/csv'
|
||||
)
|
||||
|
||||
if (status !== true) {
|
||||
Quasar.Notify.create({
|
||||
message: 'Browser denied file download...',
|
||||
color: 'negative',
|
||||
icon: null
|
||||
})
|
||||
}
|
||||
},
|
||||
convertMarkdown(text) {
|
||||
const converter = new showdown.Converter()
|
||||
converter.setFlavor('github')
|
||||
converter.setOption('simpleLineBreaks', true)
|
||||
return converter.makeHtml(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.g) {
|
||||
window.g = Vue.reactive({
|
||||
offline: !navigator.onLine,
|
||||
visibleDrawer: false,
|
||||
extensions: [],
|
||||
user: null,
|
||||
wallet: {},
|
||||
fiatBalance: 0,
|
||||
exchangeRate: 0,
|
||||
fiatTracking: false,
|
||||
wallets: [],
|
||||
payments: [],
|
||||
langs: [],
|
||||
walletEventListeners: [],
|
||||
updatePayments: false,
|
||||
updatePaymentsHash: ''
|
||||
})
|
||||
}
|
||||
|
||||
window.windowMixin = {
|
||||
i18n: window.i18n,
|
||||
data() {
|
||||
return {
|
||||
g: window.g,
|
||||
toggleSubs: true,
|
||||
mobileSimple: true,
|
||||
walletFlip: true,
|
||||
showAddWalletDialog: {show: false},
|
||||
isUserAuthorized: false,
|
||||
isSatsDenomination: WINDOW_SETTINGS['LNBITS_DENOMINATION'] == 'sats',
|
||||
allowedThemes: WINDOW_SETTINGS['LNBITS_THEME_OPTIONS'],
|
||||
walletEventListeners: [],
|
||||
darkChoice: this.$q.localStorage.has('lnbits.darkMode')
|
||||
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||
: true,
|
||||
borderChoice: this.$q.localStorage.has('lnbits.border')
|
||||
? this.$q.localStorage.getItem('lnbits.border')
|
||||
: USE_DEFAULT_BORDER,
|
||||
gradientChoice: this.$q.localStorage.has('lnbits.gradientBg')
|
||||
? this.$q.localStorage.getItem('lnbits.gradientBg')
|
||||
: USE_DEFAULT_GRADIENT,
|
||||
themeChoice: this.$q.localStorage.has('lnbits.theme')
|
||||
? this.$q.localStorage.getItem('lnbits.theme')
|
||||
: USE_DEFAULT_THEME,
|
||||
reactionChoice: this.$q.localStorage.has('lnbits.reactions')
|
||||
? this.$q.localStorage.getItem('lnbits.reactions')
|
||||
: USE_DEFAULT_REACTION,
|
||||
bgimageChoice: this.$q.localStorage.has('lnbits.backgroundImage')
|
||||
? this.$q.localStorage.getItem('lnbits.backgroundImage')
|
||||
: USE_DEFAULT_BGIMAGE,
|
||||
...WINDOW_SETTINGS
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
flipWallets(smallScreen) {
|
||||
this.walletFlip = !this.walletFlip
|
||||
if (this.walletFlip && smallScreen) {
|
||||
this.g.visibleDrawer = false
|
||||
}
|
||||
this.$q.localStorage.set('lnbits.walletFlip', this.walletFlip)
|
||||
},
|
||||
goToWallets() {
|
||||
this.$router.push({
|
||||
path: '/wallets'
|
||||
})
|
||||
},
|
||||
submitAddWallet() {
|
||||
if (
|
||||
this.showAddWalletDialog.name &&
|
||||
this.showAddWalletDialog.name.length > 0
|
||||
) {
|
||||
LNbits.api.createWallet(
|
||||
this.g.user.wallets[0],
|
||||
this.showAddWalletDialog.name
|
||||
)
|
||||
this.showAddWalletDialog = {show: false}
|
||||
} else {
|
||||
this.$q.notify({
|
||||
message: 'Please enter a name for the wallet',
|
||||
color: 'negative'
|
||||
})
|
||||
}
|
||||
},
|
||||
simpleMobile() {
|
||||
this.$q.localStorage.set('lnbits.mobileSimple', !this.mobileSimple)
|
||||
this.refreshRoute()
|
||||
},
|
||||
paymentEvents() {
|
||||
this.g.walletEventListeners = this.g.walletEventListeners || []
|
||||
this.g.user.wallets.forEach(wallet => {
|
||||
if (!this.g.walletEventListeners.includes(wallet.id)) {
|
||||
this.g.walletEventListeners.push(wallet.id)
|
||||
LNbits.events.onInvoicePaid(wallet, data => {
|
||||
const walletIndex = this.g.user.wallets.findIndex(
|
||||
w => w.id === wallet.id
|
||||
)
|
||||
if (walletIndex !== -1) {
|
||||
//needed for balance being deducted
|
||||
let satBalance = data.wallet_balance
|
||||
if (data.payment.amount < 0) {
|
||||
satBalance = data.wallet_balance += data.payment.amount / 1000
|
||||
}
|
||||
//update the wallet
|
||||
Object.assign(this.g.user.wallets[walletIndex], {
|
||||
sat: satBalance,
|
||||
msat: data.wallet_balance * 1000,
|
||||
fsat: data.wallet_balance.toLocaleString()
|
||||
})
|
||||
//update the current wallet
|
||||
if (this.g.wallet.id === data.payment.wallet_id) {
|
||||
Object.assign(this.g.wallet, this.g.user.wallets[walletIndex])
|
||||
|
||||
//if on the wallet page and payment is incoming trigger the eventReaction
|
||||
if (
|
||||
data.payment.amount > 0 &&
|
||||
window.location.pathname === '/wallet'
|
||||
) {
|
||||
eventReaction(data.wallet_balance * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.g.updatePaymentsHash = data.payment.payment_hash
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
selectWallet(wallet) {
|
||||
Object.assign(this.g.wallet, wallet)
|
||||
// this.wallet = this.g.wallet
|
||||
this.g.updatePayments = !this.g.updatePayments
|
||||
this.balance = parseInt(wallet.balance_msat / 1000)
|
||||
const currentPath = this.$route.path
|
||||
if (currentPath !== '/wallet') {
|
||||
this.$router.push({
|
||||
path: '/wallet',
|
||||
query: {wal: this.g.wallet.id}
|
||||
})
|
||||
} else {
|
||||
this.$router.replace({
|
||||
path: '/wallet',
|
||||
query: {wal: this.g.wallet.id}
|
||||
})
|
||||
}
|
||||
},
|
||||
formatBalance(amount) {
|
||||
if (LNBITS_DENOMINATION != 'sats') {
|
||||
return LNbits.utils.formatCurrency(amount / 100, LNBITS_DENOMINATION)
|
||||
} else {
|
||||
return LNbits.utils.formatSat(amount) + ' sats'
|
||||
}
|
||||
},
|
||||
changeTheme(newValue) {
|
||||
document.body.setAttribute('data-theme', newValue)
|
||||
this.$q.localStorage.set('lnbits.theme', newValue)
|
||||
this.themeChoice = newValue
|
||||
},
|
||||
applyGradient() {
|
||||
if (this.gradientChoice) {
|
||||
document.body.classList.add('gradient-bg')
|
||||
this.$q.localStorage.set('lnbits.gradientBg', true)
|
||||
// Ensure dark mode is enabled when gradient background is applied
|
||||
if (!this.$q.dark.isActive) {
|
||||
this.toggleDarkMode()
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove('gradient-bg')
|
||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
||||
}
|
||||
},
|
||||
applyBackgroundImage() {
|
||||
if (this.bgimageChoice == 'null') this.bgimageChoice = ''
|
||||
if (this.bgimageChoice == '') {
|
||||
document.body.classList.remove('bg-image')
|
||||
} else {
|
||||
document.body.classList.add('bg-image')
|
||||
document.body.style.setProperty(
|
||||
'--background',
|
||||
`url(${this.bgimageChoice})`
|
||||
)
|
||||
}
|
||||
this.$q.localStorage.set('lnbits.backgroundImage', this.bgimageChoice)
|
||||
},
|
||||
applyBorder() {
|
||||
// Remove any existing border classes
|
||||
document.body.classList.forEach(cls => {
|
||||
if (cls.endsWith('-border')) {
|
||||
document.body.classList.remove(cls)
|
||||
}
|
||||
})
|
||||
this.$q.localStorage.setItem('lnbits.border', this.borderChoice)
|
||||
document.body.classList.add(this.borderChoice)
|
||||
},
|
||||
toggleDarkMode() {
|
||||
this.$q.dark.toggle()
|
||||
this.darkChoice = this.$q.dark.isActive
|
||||
this.$q.localStorage.set('lnbits.darkMode', this.$q.dark.isActive)
|
||||
if (!this.$q.dark.isActive) {
|
||||
this.gradientChoice = false
|
||||
this.applyGradient()
|
||||
}
|
||||
},
|
||||
copyText(text, message, position) {
|
||||
Quasar.copyToClipboard(text).then(() => {
|
||||
Quasar.Notify.create({
|
||||
message: message || 'Copied to clipboard!',
|
||||
position: position || 'bottom'
|
||||
})
|
||||
})
|
||||
},
|
||||
async checkUsrInUrl() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const usr = params.get('usr')
|
||||
if (!usr) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isUserAuthorized) {
|
||||
await LNbits.api.loginUsr(usr)
|
||||
}
|
||||
|
||||
params.delete('usr')
|
||||
const cleanQueryPrams = params.size ? `?${params.toString()}` : ''
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
window.location.pathname + cleanQueryPrams
|
||||
)
|
||||
} finally {
|
||||
this.isUserAuthorized = !!this.$q.cookies.get(
|
||||
'is_lnbits_user_authorized'
|
||||
)
|
||||
}
|
||||
},
|
||||
async logout() {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Do you really want to logout?' +
|
||||
' Please visit "My Account" page to check your credentials!'
|
||||
)
|
||||
.onOk(async () => {
|
||||
try {
|
||||
await LNbits.api.logout()
|
||||
window.location = '/'
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
})
|
||||
},
|
||||
themeParams() {
|
||||
const url = new URL(window.location.href)
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const fields = ['theme', 'dark', 'gradient']
|
||||
const toBoolean = value =>
|
||||
value.trim().toLowerCase() === 'true' || value === '1'
|
||||
|
||||
// Check if any of the relevant parameters ('theme', 'dark', 'gradient') are present in the URL.
|
||||
if (fields.some(param => params.has(param))) {
|
||||
const theme = params.get('theme')
|
||||
const darkMode = params.get('dark')
|
||||
const gradient = params.get('gradient')
|
||||
const border = params.get('border')
|
||||
|
||||
if (theme && this.allowedThemes.includes(theme.trim().toLowerCase())) {
|
||||
const normalizedTheme = theme.trim().toLowerCase()
|
||||
document.body.setAttribute('data-theme', normalizedTheme)
|
||||
this.$q.localStorage.set('lnbits.theme', normalizedTheme)
|
||||
}
|
||||
|
||||
if (darkMode) {
|
||||
const isDark = toBoolean(darkMode)
|
||||
this.$q.localStorage.set('lnbits.darkMode', isDark)
|
||||
if (!isDark) {
|
||||
this.$q.localStorage.set('lnbits.gradientBg', false)
|
||||
}
|
||||
}
|
||||
|
||||
if (gradient) {
|
||||
const isGradient = toBoolean(gradient)
|
||||
this.$q.localStorage.set('lnbits.gradientBg', isGradient)
|
||||
if (isGradient) {
|
||||
this.$q.localStorage.set('lnbits.darkMode', true)
|
||||
}
|
||||
}
|
||||
if (border) {
|
||||
this.$q.localStorage.set('lnbits.border', border)
|
||||
}
|
||||
|
||||
// Remove processed parameters
|
||||
fields.forEach(param => params.delete(param))
|
||||
|
||||
window.history.replaceState(null, null, url.pathname)
|
||||
}
|
||||
},
|
||||
refreshRoute() {
|
||||
const path = window.location.pathname
|
||||
console.log(path)
|
||||
|
||||
this.$router.push('/temp').then(() => {
|
||||
this.$router.replace({path})
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.$q.dark.set(
|
||||
this.$q.localStorage.has('lnbits.darkMode')
|
||||
? this.$q.localStorage.getItem('lnbits.darkMode')
|
||||
: true
|
||||
)
|
||||
Chart.defaults.color = this.$q.dark.isActive ? '#fff' : '#000'
|
||||
this.changeTheme(this.themeChoice)
|
||||
this.applyBorder()
|
||||
if (this.$q.dark.isActive) {
|
||||
this.applyGradient()
|
||||
}
|
||||
this.applyBackgroundImage()
|
||||
|
||||
let locale = this.$q.localStorage.getItem('lnbits.lang')
|
||||
if (locale) {
|
||||
window.LOCALE = locale
|
||||
window.i18n.global.locale = locale
|
||||
}
|
||||
|
||||
this.g.langs = window.langs ?? []
|
||||
|
||||
addEventListener('offline', event => {
|
||||
this.g.offline = true
|
||||
})
|
||||
|
||||
addEventListener('online', event => {
|
||||
this.g.offline = false
|
||||
})
|
||||
|
||||
if (window.user) {
|
||||
this.g.user = Vue.reactive(window.LNbits.map.user(window.user))
|
||||
}
|
||||
if (window.wallet) {
|
||||
this.g.wallet = Vue.reactive(window.LNbits.map.wallet(window.wallet))
|
||||
}
|
||||
if (window.extensions) {
|
||||
this.g.extensions = Vue.reactive(window.extensions)
|
||||
}
|
||||
await this.checkUsrInUrl()
|
||||
this.themeParams()
|
||||
this.walletFlip = this.$q.localStorage.getItem('lnbits.walletFlip')
|
||||
if (
|
||||
this.$q.screen.gt.sm ||
|
||||
this.$q.localStorage.getItem('lnbits.mobileSimple') == false
|
||||
) {
|
||||
this.mobileSimple = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.g.user) {
|
||||
this.paymentEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.decryptLnurlPayAES = (success_action, preimage) => {
|
||||
let keyb = new Uint8Array(
|
||||
preimage.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))
|
||||
)
|
||||
|
||||
return crypto.subtle
|
||||
.importKey('raw', keyb, {name: 'AES-CBC', length: 256}, false, ['decrypt'])
|
||||
.then(key => {
|
||||
let ivb = Uint8Array.from(window.atob(success_action.iv), c =>
|
||||
c.charCodeAt(0)
|
||||
)
|
||||
let ciphertextb = Uint8Array.from(
|
||||
window.atob(success_action.ciphertext),
|
||||
c => c.charCodeAt(0)
|
||||
)
|
||||
|
||||
return crypto.subtle.decrypt({name: 'AES-CBC', iv: ivb}, key, ciphertextb)
|
||||
})
|
||||
.then(valueb => {
|
||||
let decoder = new TextDecoder('utf-8')
|
||||
return decoder.decode(valueb)
|
||||
})
|
||||
}
|
||||
|
||||
+275
-80
@@ -21,89 +21,26 @@ window.app.component('lnbits-fsat', {
|
||||
}
|
||||
})
|
||||
|
||||
window.app.component('lnbits-wallet-list', {
|
||||
mixins: [window.windowMixin],
|
||||
template: '#lnbits-wallet-list',
|
||||
props: ['balance'],
|
||||
data() {
|
||||
return {
|
||||
activeWallet: null,
|
||||
balance: 0,
|
||||
showForm: false,
|
||||
walletName: '',
|
||||
LNBITS_DENOMINATION: LNBITS_DENOMINATION
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createWallet() {
|
||||
LNbits.api.createWallet(this.g.user.wallets[0], this.walletName)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.addEventListener('updateWalletBalance', this.updateWalletBalance)
|
||||
}
|
||||
})
|
||||
|
||||
window.app.component('lnbits-extension-list', {
|
||||
mixins: [window.windowMixin],
|
||||
template: '#lnbits-extension-list',
|
||||
data() {
|
||||
return {
|
||||
extensions: [],
|
||||
searchTerm: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.user.extensions': {
|
||||
handler(newExtensions) {
|
||||
this.loadExtensions()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userExtensions() {
|
||||
return this.updateUserExtensions(this.searchTerm)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
||||
this.extensions = data
|
||||
.map(extension => LNbits.map.extension(extension))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
updateUserExtensions(filterBy) {
|
||||
const path = window.location.pathname
|
||||
const userExtensions = this.g.user.extensions
|
||||
|
||||
return this.extensions
|
||||
.filter(o => userExtensions.includes(o.code))
|
||||
.filter(o => {
|
||||
if (!filterBy) return true
|
||||
return `${o.code} ${o.name} ${o.short_description} ${o.url}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(filterBy.toLocaleLowerCase())
|
||||
})
|
||||
.map(obj => {
|
||||
obj.isActive = path.startsWith(obj.url)
|
||||
return obj
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadExtensions()
|
||||
}
|
||||
})
|
||||
|
||||
window.app.component('lnbits-manage', {
|
||||
mixins: [window.windowMixin],
|
||||
template: '#lnbits-manage',
|
||||
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||
computed: {
|
||||
showAdmin() {
|
||||
return this.LNBITS_ADMIN_UI
|
||||
},
|
||||
showUsers() {
|
||||
return this.LNBITS_ADMIN_UI
|
||||
},
|
||||
showNode() {
|
||||
return this.LNBITS_NODE_UI
|
||||
},
|
||||
showAudit() {
|
||||
return this.LNBITS_AUDIT_ENABLED
|
||||
},
|
||||
showExtensions() {
|
||||
return this.LNBITS_EXTENSIONS_DEACTIVATE_ALL === false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive(path) {
|
||||
return window.location.pathname === path
|
||||
@@ -656,3 +593,261 @@ window.app.component('separator-text', {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.app.component('lnbits-node-ranks', {
|
||||
props: ['ranks'],
|
||||
data() {
|
||||
return {
|
||||
stats: [
|
||||
{label: 'Capacity', key: 'capacity'},
|
||||
{label: 'Channels', key: 'channelcount'},
|
||||
{label: 'Age', key: 'age'},
|
||||
{label: 'Growth', key: 'growth'},
|
||||
{label: 'Availability', key: 'availability'}
|
||||
]
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-card class='q-my-none'>
|
||||
<div class='column q-ma-md'>
|
||||
<h5 class='text-subtitle1 text-bold q-my-none'>1ml Node Rank</h5>
|
||||
<div v-for='stat in stats' class='q-gutter-sm'>
|
||||
<div class='row items-center'>
|
||||
<div class='col-9'>{{ stat.label }}</div>
|
||||
<div class='col-3 text-subtitle1 text-bold'>
|
||||
{{ (ranks && ranks[stat.key]) ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-channel-stats', {
|
||||
props: ['stats'],
|
||||
data() {
|
||||
return {
|
||||
states: [
|
||||
{label: 'Active', value: 'active', color: 'green'},
|
||||
{label: 'Pending', value: 'pending', color: 'orange'},
|
||||
{label: 'Inactive', value: 'inactive', color: 'grey'},
|
||||
{label: 'Closed', value: 'closed', color: 'red'}
|
||||
]
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-card>
|
||||
<div class='column q-ma-md'>
|
||||
<h5 class='text-subtitle1 text-bold q-my-none'>Channels</h5>
|
||||
<div v-for='state in states' class='q-gutter-sm'>
|
||||
<div class='row'>
|
||||
<div class='col-9'>
|
||||
<q-badge rounded size='md' :color='state.color'>{{ state.label }}</q-badge>
|
||||
</div>
|
||||
<div class='col-3 text-subtitle1 text-bold'>
|
||||
{{ (stats?.counts && stats.counts[state.value]) ?? "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-stat', {
|
||||
props: ['title', 'amount', 'msat', 'btc'],
|
||||
computed: {
|
||||
value() {
|
||||
return (
|
||||
this.amount ??
|
||||
(this.btc
|
||||
? LNbits.utils.formatSat(this.btc)
|
||||
: LNbits.utils.formatMsat(this.msat))
|
||||
)
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class='text-overline text-primary'>
|
||||
{{ title }}
|
||||
</div>
|
||||
<div>
|
||||
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
|
||||
<span class='text-h5' v-if='msat != undefined'>sats</span>
|
||||
<span class='text-h5' v-if='btc != undefined'>BTC</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-node-qrcode', {
|
||||
props: ['info'],
|
||||
mixins: [window.windowMixin],
|
||||
template: `
|
||||
<q-card class="my-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<div style="text-align: center">
|
||||
<vue-qrcode
|
||||
:value="info.addresses[0]"
|
||||
:options="{width: 250}"
|
||||
v-if='info.addresses[0]'
|
||||
class="rounded-borders"
|
||||
></vue-qrcode>
|
||||
<div v-else class='text-subtitle1'>
|
||||
No addresses available
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-actions vertical>
|
||||
<q-btn
|
||||
dense
|
||||
unelevated
|
||||
size="md"
|
||||
@click="copyText(info.id)"
|
||||
>Public Key<q-tooltip> Click to copy </q-tooltip>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-channel-balance', {
|
||||
props: ['balance', 'color'],
|
||||
methods: {
|
||||
formatMsat(msat) {
|
||||
return LNbits.utils.formatMsat(msat)
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="row items-center justify-between">
|
||||
<span class="text-weight-thin">
|
||||
Local: {{ formatMsat(balance.local_msat) }}
|
||||
sats
|
||||
</span>
|
||||
<span class="text-weight-thin">
|
||||
Remote: {{ formatMsat(balance.remote_msat) }}
|
||||
sats
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-linear-progress
|
||||
rounded
|
||||
size="25px"
|
||||
:value="balance.local_msat / balance.total_msat"
|
||||
:color="color"
|
||||
:style="\`color: #\${this.color}\`"
|
||||
>
|
||||
<div class="absolute-full flex flex-center">
|
||||
<q-badge
|
||||
color="white"
|
||||
text-color="accent"
|
||||
:label="formatMsat(balance.total_msat) + ' sats'"
|
||||
>
|
||||
{{ balance.alias }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</q-linear-progress>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-date', {
|
||||
props: ['ts'],
|
||||
computed: {
|
||||
date() {
|
||||
return LNbits.utils.formatDate(this.ts)
|
||||
},
|
||||
dateFrom() {
|
||||
return moment.utc(this.date).local().fromNow()
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<q-tooltip>{{ this.date }}</q-tooltip>
|
||||
{{ this.dateFrom }}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-node-info', {
|
||||
props: ['info'],
|
||||
data() {
|
||||
return {
|
||||
showDialog: false
|
||||
}
|
||||
},
|
||||
mixins: [window.windowMixin],
|
||||
methods: {
|
||||
shortenNodeId(nodeId) {
|
||||
return nodeId
|
||||
? nodeId.substring(0, 5) + '...' + nodeId.substring(nodeId.length - 5)
|
||||
: '...'
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class='row items-baseline q-gutter-x-sm'>
|
||||
<div class='text-h4 text-bold'>{{ this.info.alias }}</div>
|
||||
<div class='row items-center q-gutter-sm'>
|
||||
<div class='text-subtitle1 text-light'>{{ this.info.backend_name }}</div>
|
||||
<q-badge
|
||||
:style='\`background-color: #\${this.info.color}\`'
|
||||
class='text-bold'
|
||||
>
|
||||
#{{ this.info.color }}
|
||||
</q-badge>
|
||||
<div class='text-bold'>{{ shortenNodeId(this.info.id) }}</div>
|
||||
<q-btn
|
||||
size='xs'
|
||||
flat
|
||||
dense
|
||||
icon='content_paste'
|
||||
@click='copyText(info.id)'
|
||||
></q-btn>
|
||||
<q-btn
|
||||
size='xs'
|
||||
flat
|
||||
dense
|
||||
icon='qr_code'
|
||||
@click='showDialog = true'
|
||||
></q-btn>
|
||||
</div>
|
||||
<q-dialog v-model="showDialog">
|
||||
<lnbits-node-qrcode :info='info'></lnbits-node-qrcode>
|
||||
</q-dialog>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
window.app.component('lnbits-stat', {
|
||||
props: ['title', 'amount', 'msat', 'btc'],
|
||||
computed: {
|
||||
value() {
|
||||
return (
|
||||
this.amount ??
|
||||
(this.btc
|
||||
? LNbits.utils.formatSat(this.btc)
|
||||
: LNbits.utils.formatMsat(this.msat))
|
||||
)
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class='text-overline text-primary'>
|
||||
{{ title }}
|
||||
</div>
|
||||
<div>
|
||||
<span class='text-h4 text-bold q-my-none'>{{ value }}</span>
|
||||
<span class='text-h5' v-if='msat != undefined'>sats</span>
|
||||
<span class='text-h5' v-if='btc != undefined'>BTC</span>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
`
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
window.app.component('lnbits-admin-assets-config', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-assets-config',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
newAllowedAssetMimeType: '',
|
||||
newNoLimitUser: ''
|
||||
}
|
||||
},
|
||||
async created() {},
|
||||
methods: {
|
||||
addAllowedAssetMimeType() {
|
||||
if (this.newAllowedAssetMimeType) {
|
||||
this.removeAllowedAssetMimeType(this.newAllowedAssetMimeType)
|
||||
this.formData.lnbits_assets_allowed_mime_types.push(
|
||||
this.newAllowedAssetMimeType
|
||||
)
|
||||
this.newAllowedAssetMimeType = ''
|
||||
}
|
||||
},
|
||||
removeAllowedAssetMimeType(type) {
|
||||
const index = this.formData.lnbits_assets_allowed_mime_types.indexOf(type)
|
||||
if (index !== -1) {
|
||||
this.formData.lnbits_assets_allowed_mime_types.splice(index, 1)
|
||||
}
|
||||
},
|
||||
addNewNoLimitUser() {
|
||||
if (this.newNoLimitUser) {
|
||||
this.removeNoLimitUser(this.newNoLimitUser)
|
||||
this.formData.lnbits_assets_no_limit_users.push(this.newNoLimitUser)
|
||||
this.newNoLimitUser = ''
|
||||
}
|
||||
},
|
||||
removeNoLimitUser(user) {
|
||||
if (user) {
|
||||
this.formData.lnbits_assets_no_limit_users =
|
||||
this.formData.lnbits_assets_no_limit_users.filter(u => u !== user)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
window.app.component('lnbits-admin-audit', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-audit',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
formAddIncludePath: '',
|
||||
formAddExcludePath: '',
|
||||
formAddIncludeResponseCode: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addIncludePath() {
|
||||
if (this.formAddIncludePath === '') {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_include_paths
|
||||
if (!paths.includes(this.formAddIncludePath)) {
|
||||
this.formData.lnbits_audit_include_paths = [
|
||||
...paths,
|
||||
this.formAddIncludePath
|
||||
]
|
||||
}
|
||||
this.formAddIncludePath = ''
|
||||
},
|
||||
removeIncludePath(path) {
|
||||
this.formData.lnbits_audit_include_paths =
|
||||
this.formData.lnbits_audit_include_paths.filter(p => p !== path)
|
||||
},
|
||||
addExcludePath() {
|
||||
if (this.formAddExcludePath === '') {
|
||||
return
|
||||
}
|
||||
const paths = this.formData.lnbits_audit_exclude_paths
|
||||
if (!paths.includes(this.formAddExcludePath)) {
|
||||
this.formData.lnbits_audit_exclude_paths = [
|
||||
...paths,
|
||||
this.formAddExcludePath
|
||||
]
|
||||
}
|
||||
this.formAddExcludePath = ''
|
||||
},
|
||||
|
||||
removeExcludePath(path) {
|
||||
this.formData.lnbits_audit_exclude_paths =
|
||||
this.formData.lnbits_audit_exclude_paths.filter(p => p !== path)
|
||||
},
|
||||
addIncludeResponseCode() {
|
||||
if (this.formAddIncludeResponseCode === '') {
|
||||
return
|
||||
}
|
||||
const codes = this.formData.lnbits_audit_http_response_codes
|
||||
if (!codes.includes(this.formAddIncludeResponseCode)) {
|
||||
this.formData.lnbits_audit_http_response_codes = [
|
||||
...codes,
|
||||
this.formAddIncludeResponseCode
|
||||
]
|
||||
}
|
||||
this.formAddIncludeResponseCode = ''
|
||||
},
|
||||
removeIncludeResponseCode(code) {
|
||||
this.formData.lnbits_audit_http_response_codes =
|
||||
this.formData.lnbits_audit_http_response_codes.filter(c => c !== code)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
window.app.component('lnbits-admin-exchange-providers', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-exchange-providers',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
exchangeData: {
|
||||
selectedProvider: null,
|
||||
showTickerConversion: false,
|
||||
convertFromTicker: null,
|
||||
convertToTicker: null
|
||||
},
|
||||
exchangesTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
align: 'left',
|
||||
label: 'Exchange Name',
|
||||
field: 'name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'api_url',
|
||||
align: 'left',
|
||||
label: 'URL',
|
||||
field: 'api_url',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
align: 'left',
|
||||
label: 'JSON Path',
|
||||
field: 'path',
|
||||
sortable: false
|
||||
},
|
||||
|
||||
{
|
||||
name: 'exclude_to',
|
||||
align: 'left',
|
||||
label: 'Exclude Currencies',
|
||||
field: 'exclude_to',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: 'ticker_conversion',
|
||||
align: 'left',
|
||||
label: 'Ticker Conversion',
|
||||
field: 'ticker_conversion',
|
||||
sortable: false
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
sortBy: 'name',
|
||||
rowsPerPage: 100,
|
||||
page: 1,
|
||||
rowsNumber: 100
|
||||
},
|
||||
search: null,
|
||||
hideEmpty: true
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getExchangeRateHistory()
|
||||
},
|
||||
created() {
|
||||
const hash = window.location.hash.replace('#', '')
|
||||
if (hash === 'exchange_providers') {
|
||||
this.showExchangeProvidersTab(hash)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExchangeRateHistory() {
|
||||
LNbits.api
|
||||
.request('GET', '/api/v1/rate/history', this.g.user.wallets[0].inkey)
|
||||
.then(response => {
|
||||
this.initExchangeChart(response.data)
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
showExchangeProvidersTab(tabName) {
|
||||
if (tabName === 'exchange_providers') {
|
||||
this.getExchangeRateHistory()
|
||||
}
|
||||
},
|
||||
addExchangeProvider() {
|
||||
this.formData.lnbits_exchange_rate_providers = [
|
||||
{
|
||||
name: '',
|
||||
api_url: '',
|
||||
path: '',
|
||||
exclude_to: []
|
||||
},
|
||||
...this.formData.lnbits_exchange_rate_providers
|
||||
]
|
||||
},
|
||||
removeExchangeProvider(provider) {
|
||||
this.formData.lnbits_exchange_rate_providers =
|
||||
this.formData.lnbits_exchange_rate_providers.filter(p => p !== provider)
|
||||
},
|
||||
removeExchangeTickerConversion(provider, ticker) {
|
||||
provider.ticker_conversion = provider.ticker_conversion.filter(
|
||||
t => t !== ticker
|
||||
)
|
||||
this.formData.touch = null
|
||||
},
|
||||
addExchangeTickerConversion() {
|
||||
if (!this.exchangeData.selectedProvider) {
|
||||
return
|
||||
}
|
||||
this.exchangeData.selectedProvider.ticker_conversion.push(
|
||||
`${this.exchangeData.convertFromTicker}:${this.exchangeData.convertToTicker}`
|
||||
)
|
||||
this.formData.touch = null
|
||||
this.exchangeData.showTickerConversion = false
|
||||
},
|
||||
showTickerConversionDialog(provider) {
|
||||
this.exchangeData.convertFromTicker = null
|
||||
this.exchangeData.convertToTicker = null
|
||||
this.exchangeData.selectedProvider = provider
|
||||
this.exchangeData.showTickerConversion = true
|
||||
},
|
||||
initExchangeChart(data) {
|
||||
const xValues = data.map(d =>
|
||||
Quasar.date.formatDate(new Date(d.timestamp * 1000), 'HH:mm')
|
||||
)
|
||||
const exchanges = [
|
||||
...this.formData.lnbits_exchange_rate_providers,
|
||||
{name: 'LNbits'}
|
||||
]
|
||||
const datasets = exchanges.map(exchange => ({
|
||||
label: exchange.name,
|
||||
data: data.map(d => d.rates[exchange.name]),
|
||||
pointStyle: true,
|
||||
borderWidth: exchange.name === 'LNbits' ? 4 : 1,
|
||||
tension: 0.4
|
||||
}))
|
||||
this.exchangeRatesChart = new Chart(
|
||||
this.$refs.exchangeRatesChart.getContext('2d'),
|
||||
{
|
||||
type: 'line',
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
labels: xValues,
|
||||
datasets
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
window.app.component('lnbits-admin-extensions', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-extensions',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
formAddExtensionsManifest: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addExtensionsManifest() {
|
||||
const addManifest = this.formAddExtensionsManifest.trim()
|
||||
const manifests = this.formData.lnbits_extensions_manifests
|
||||
if (
|
||||
addManifest &&
|
||||
addManifest.length &&
|
||||
!manifests.includes(addManifest)
|
||||
) {
|
||||
this.formData.lnbits_extensions_manifests = [...manifests, addManifest]
|
||||
this.formAddExtensionsManifest = ''
|
||||
}
|
||||
},
|
||||
removeExtensionsManifest(manifest) {
|
||||
const manifests = this.formData.lnbits_extensions_manifests
|
||||
this.formData.lnbits_extensions_manifests = manifests.filter(
|
||||
m => m !== manifest
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
window.app.component('lnbits-admin-fiat-providers', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-fiat-providers',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
formAddStripeUser: '',
|
||||
hideInputToggle: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addStripeAllowedUser() {
|
||||
const addUser = this.formAddStripeUser || ''
|
||||
if (
|
||||
addUser.length &&
|
||||
!this.formData.stripe_limits.allowed_users.includes(addUser)
|
||||
) {
|
||||
this.formData.stripe_limits.allowed_users = [
|
||||
...this.formData.stripe_limits.allowed_users,
|
||||
addUser
|
||||
]
|
||||
this.formAddStripeUser = ''
|
||||
}
|
||||
},
|
||||
removeStripeAllowedUser(user) {
|
||||
this.formData.stripe_limits.allowed_users =
|
||||
this.formData.stripe_limits.allowed_users.filter(u => u !== user)
|
||||
},
|
||||
checkFiatProvider(providerName) {
|
||||
LNbits.api
|
||||
.request('PUT', `/api/v1/fiat/check/${providerName}`)
|
||||
.then(response => {
|
||||
response
|
||||
const data = response.data
|
||||
Quasar.Notify.create({
|
||||
type: data.success ? 'positive' : 'warning',
|
||||
message: data.message,
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
}
|
||||
})
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
window.app.component('lnbits-funding-sources', {
|
||||
template: '#lnbits-funding-sources',
|
||||
window.app.component('lnbits-admin-funding-sources', {
|
||||
template: '#lnbits-admin-funding-sources',
|
||||
mixins: [window.windowMixin],
|
||||
props: ['form-data', 'allowed-funding-sources'],
|
||||
methods: {
|
||||
@@ -100,7 +100,9 @@ window.app.component('lnbits-funding-sources', {
|
||||
lnd_grpc_endpoint: 'Endpoint',
|
||||
lnd_grpc_cert: 'Certificate',
|
||||
lnd_grpc_port: 'Port',
|
||||
lnd_grpc_admin_macaroon: 'Admin Macaroon',
|
||||
lnd_grpc_macaroon: 'GRPC Macaroon',
|
||||
lnd_grpc_invoice_macaroon: 'GRPC Invoice Macaroon',
|
||||
lnd_grpc_admin_macaroon: 'GRPC Admin Macaroon',
|
||||
lnd_grpc_macaroon_encrypted: 'Encrypted Macaroon'
|
||||
}
|
||||
],
|
||||
@@ -0,0 +1,24 @@
|
||||
window.app.component('lnbits-admin-funding', {
|
||||
props: ['is-super-user', 'form-data', 'settings'],
|
||||
template: '#lnbits-admin-funding',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
auditData: []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getAudit()
|
||||
},
|
||||
methods: {
|
||||
getAudit() {
|
||||
LNbits.api
|
||||
// TODO: should not use admin key here
|
||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||
.then(response => {
|
||||
this.auditData = response.data
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
window.app.component('lnbits-admin-notifications', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-notifications',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
nostrNotificationIdentifier: '',
|
||||
emailNotificationAddress: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendTestEmail() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/admin/api/v1/testemail',
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
if (response.data.status === 'error') {
|
||||
throw new Error(response.data.message)
|
||||
}
|
||||
this.$q.notify({
|
||||
message: 'Test email sent!',
|
||||
color: 'positive'
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.$q.notify({
|
||||
message: error.message,
|
||||
color: 'negative'
|
||||
})
|
||||
})
|
||||
},
|
||||
addNostrNotificationIdentifier() {
|
||||
const identifer = this.nostrNotificationIdentifier.trim()
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
if (identifer && identifer.length && !identifiers.includes(identifer)) {
|
||||
this.formData.lnbits_nostr_notifications_identifiers = [
|
||||
...identifiers,
|
||||
identifer
|
||||
]
|
||||
this.nostrNotificationIdentifier = ''
|
||||
}
|
||||
},
|
||||
removeNostrNotificationIdentifier(identifer) {
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
|
||||
m => m !== identifer
|
||||
)
|
||||
},
|
||||
addEmailNotificationAddress() {
|
||||
const email = this.emailNotificationAddress.trim()
|
||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||
if (email && email.length && !emails.includes(email)) {
|
||||
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
|
||||
this.emailNotificationAddress = ''
|
||||
}
|
||||
},
|
||||
removeEmailNotificationAddress(email) {
|
||||
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||
this.formData.lnbits_email_notifications_to_emails = emails.filter(
|
||||
m => m !== email
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
window.app.component('lnbits-admin-security', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-security',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
logs: [],
|
||||
formBlockedIPs: '',
|
||||
serverlogEnabled: false,
|
||||
nostrAcceptedUrl: '',
|
||||
formAllowedIPs: '',
|
||||
formCallbackUrlRule: ''
|
||||
}
|
||||
},
|
||||
created() {},
|
||||
methods: {
|
||||
addAllowedIPs() {
|
||||
const allowedIPs = this.formAllowedIPs.trim()
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
if (
|
||||
allowedIPs &&
|
||||
allowedIPs.length &&
|
||||
!allowed_ips.includes(allowedIPs)
|
||||
) {
|
||||
this.formData.lnbits_allowed_ips = [...allowed_ips, allowedIPs]
|
||||
this.formAllowedIPs = ''
|
||||
}
|
||||
},
|
||||
removeAllowedIPs(allowed_ip) {
|
||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||
this.formData.lnbits_allowed_ips = allowed_ips.filter(
|
||||
a => a !== allowed_ip
|
||||
)
|
||||
},
|
||||
addBlockedIPs() {
|
||||
const blockedIPs = this.formBlockedIPs.trim()
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
if (
|
||||
blockedIPs &&
|
||||
blockedIPs.length &&
|
||||
!blocked_ips.includes(blockedIPs)
|
||||
) {
|
||||
this.formData.lnbits_blocked_ips = [...blocked_ips, blockedIPs]
|
||||
this.formBlockedIPs = ''
|
||||
}
|
||||
},
|
||||
removeBlockedIPs(blocked_ip) {
|
||||
const blocked_ips = this.formData.lnbits_blocked_ips
|
||||
this.formData.lnbits_blocked_ips = blocked_ips.filter(
|
||||
b => b !== blocked_ip
|
||||
)
|
||||
},
|
||||
addCallbackUrlRule() {
|
||||
const allowedCallback = this.formCallbackUrlRule.trim()
|
||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
||||
if (
|
||||
allowedCallback &&
|
||||
allowedCallback.length &&
|
||||
!allowedCallbacks.includes(allowedCallback)
|
||||
) {
|
||||
this.formData.lnbits_callback_url_rules = [
|
||||
...allowedCallbacks,
|
||||
allowedCallback
|
||||
]
|
||||
this.formCallbackUrlRule = ''
|
||||
}
|
||||
},
|
||||
removeCallbackUrlRule(allowedCallback) {
|
||||
const allowedCallbacks = this.formData.lnbits_callback_url_rules
|
||||
this.formData.lnbits_callback_url_rules = allowedCallbacks.filter(
|
||||
a => a !== allowedCallback
|
||||
)
|
||||
},
|
||||
addNostrUrl() {
|
||||
const url = this.nostrAcceptedUrl.trim()
|
||||
this.removeNostrUrl(url)
|
||||
this.formData.nostr_absolute_request_urls.push(url)
|
||||
this.nostrAcceptedUrl = ''
|
||||
},
|
||||
removeNostrUrl(url) {
|
||||
this.formData.nostr_absolute_request_urls =
|
||||
this.formData.nostr_absolute_request_urls.filter(b => b !== url)
|
||||
},
|
||||
async toggleServerLog() {
|
||||
this.serverlogEnabled = !this.serverlogEnabled
|
||||
if (this.serverlogEnabled) {
|
||||
const wsProto = location.protocol !== 'http:' ? 'wss://' : 'ws://'
|
||||
const digestHex = await LNbits.utils.digestMessage(this.g.user.id)
|
||||
const localUrl =
|
||||
wsProto +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/api/v1/ws/' +
|
||||
digestHex
|
||||
this.ws = new WebSocket(localUrl)
|
||||
this.ws.addEventListener('message', async ({data}) => {
|
||||
this.logs.push(data.toString())
|
||||
const scrollArea = this.$refs.logScroll
|
||||
if (scrollArea) {
|
||||
const scrollTarget = scrollArea.getScrollTarget()
|
||||
const duration = 0
|
||||
scrollArea.setScrollPosition(scrollTarget.scrollHeight, duration)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.ws.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
window.app.component('lnbits-admin-server', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-server',
|
||||
mixins: [window.windowMixin]
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
window.app.component('lnbits-admin-site-customisation', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-site-customisation',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
lnbits_theme_options: [
|
||||
'classic',
|
||||
'bitcoin',
|
||||
'flamingo',
|
||||
'cyber',
|
||||
'freedom',
|
||||
'mint',
|
||||
'autumn',
|
||||
'monochrome',
|
||||
'salvador'
|
||||
],
|
||||
colors: [
|
||||
'primary',
|
||||
'secondary',
|
||||
'accent',
|
||||
'positive',
|
||||
'negative',
|
||||
'info',
|
||||
'warning',
|
||||
'red',
|
||||
'yellow',
|
||||
'orange'
|
||||
],
|
||||
reactionOptions: [
|
||||
'none',
|
||||
'confettiBothSides',
|
||||
'confettiFireworks',
|
||||
'confettiStars',
|
||||
'confettiTop'
|
||||
],
|
||||
globalBorderOptions: [
|
||||
'retro-border',
|
||||
'hard-border',
|
||||
'neon-border',
|
||||
'no-border'
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
window.app.component('lnbits-admin-users', {
|
||||
props: ['form-data'],
|
||||
template: '#lnbits-admin-users',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
formAddUser: '',
|
||||
formAddAdmin: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAllowedUser() {
|
||||
let addUser = this.formAddUser
|
||||
let allowed_users = this.formData.lnbits_allowed_users
|
||||
if (addUser && addUser.length && !allowed_users.includes(addUser)) {
|
||||
this.formData.lnbits_allowed_users = [...allowed_users, addUser]
|
||||
this.formAddUser = ''
|
||||
}
|
||||
},
|
||||
removeAllowedUser(user) {
|
||||
let allowed_users = this.formData.lnbits_allowed_users
|
||||
this.formData.lnbits_allowed_users = allowed_users.filter(u => u !== user)
|
||||
},
|
||||
addAdminUser() {
|
||||
let addUser = this.formAddAdmin
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
if (addUser && addUser.length && !admin_users.includes(addUser)) {
|
||||
this.formData.lnbits_admin_users = [...admin_users, addUser]
|
||||
this.formAddAdmin = ''
|
||||
}
|
||||
},
|
||||
removeAdminUser(user) {
|
||||
let admin_users = this.formData.lnbits_admin_users
|
||||
this.formData.lnbits_admin_users = admin_users.filter(u => u !== user)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
window.app.component('lnbits-disclaimer', {
|
||||
template: '#lnbits-disclaimer',
|
||||
mixins: [window.windowMixin],
|
||||
computed: {
|
||||
showDisclaimer() {
|
||||
return !g.disclaimerShown && g.isUserAuthorized
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
window.app.component('lnbits-drawer', {
|
||||
template: '#lnbits-drawer',
|
||||
mixins: [window.windowMixin]
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
window.app.component('lnbits-footer', {
|
||||
template: '#lnbits-footer',
|
||||
mixins: [window.windowMixin],
|
||||
computed: {
|
||||
version() {
|
||||
return this.LNBITS_VERSION || 'unknown version'
|
||||
},
|
||||
title() {
|
||||
return `${this.SITE_TITLE}, ${this.SITE_TAGLINE}`
|
||||
},
|
||||
showFooter() {
|
||||
return this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
window.app.component('lnbits-header-wallets', {
|
||||
template: '#lnbits-header-wallets',
|
||||
mixins: [window.windowMixin]
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
window.app.component('lnbits-header', {
|
||||
template: '#lnbits-header',
|
||||
mixins: [window.windowMixin],
|
||||
computed: {
|
||||
denomination() {
|
||||
return this.LNBITS_DENOMINATION || 'sat'
|
||||
},
|
||||
hasServiceFeeMax() {
|
||||
return (
|
||||
this.g.user &&
|
||||
this.LNBITS_SERVICE_FEE_MAX &&
|
||||
this.LNBITS_SERVICE_FEE_MAX > 0
|
||||
)
|
||||
},
|
||||
serviceFeeMax() {
|
||||
return this.LNBITS_SERVICE_FEE_MAX || 0
|
||||
},
|
||||
hasServiceFee() {
|
||||
return (
|
||||
this.g.user && this.LNBITS_SERVICE_FEE && this.LNBITS_SERVICE_FEE > 0
|
||||
)
|
||||
},
|
||||
serviceFee() {
|
||||
return this.LNBITS_SERVICE_FEE || 0
|
||||
},
|
||||
hasCustomBadge() {
|
||||
return this.LNBITS_CUSTOM_BADGE && this.LNBITS_CUSTOM_BADGE != ''
|
||||
},
|
||||
customBadge() {
|
||||
return this.LNBITS_CUSTOM_BADGE || ''
|
||||
},
|
||||
customBadgeColor() {
|
||||
return this.LNBITS_CUSTOM_BADGE_COLOR || ''
|
||||
},
|
||||
title() {
|
||||
return this.SITE_TITLE
|
||||
},
|
||||
titleIsLnbits() {
|
||||
return this.SITE_TITLE == 'LNbits'
|
||||
},
|
||||
customLogoUrl() {
|
||||
return this.USE_CUSTOM_LOGO || null
|
||||
},
|
||||
userPictureUrl() {
|
||||
return this.g.user.config.picture
|
||||
},
|
||||
hasUserPicture() {
|
||||
return this.g.user && this.g.user.config && this.g.user.config.picture
|
||||
},
|
||||
showAdmin() {
|
||||
return this.g.user && (this.g.user.super_user || this.g.user.admin)
|
||||
},
|
||||
showVoidwallet() {
|
||||
return this.g.user && this.VOIDWALLET == true
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
window.app.component('lnbits-home-logos', {
|
||||
template: '#lnbits-home-logos',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
logos: [
|
||||
{
|
||||
href: 'https://github.com/ElementsProject/lightning',
|
||||
lightSrc: '/static/images/clnl.png',
|
||||
darkSrc: '/static/images/cln.png'
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/lightningnetwork/lnd',
|
||||
lightSrc: '/static/images/lnd.png',
|
||||
darkSrc: '/static/images/lnd.png'
|
||||
},
|
||||
{
|
||||
href: 'https://opennode.com',
|
||||
lightSrc: '/static/images/opennodel.png',
|
||||
darkSrc: '/static/images/opennode.png'
|
||||
},
|
||||
{
|
||||
href: 'https://lnpay.co/',
|
||||
lightSrc: '/static/images/lnpayl.png',
|
||||
darkSrc: '/static/images/lnpay.png'
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/rootzoll/raspiblitz',
|
||||
lightSrc: '/static/images/blitzl.png',
|
||||
darkSrc: '/static/images/blitz.png'
|
||||
},
|
||||
{
|
||||
href: 'https://start9.com/',
|
||||
lightSrc: '/static/images/start9l.png',
|
||||
darkSrc: '/static/images/start9.png'
|
||||
},
|
||||
{
|
||||
href: 'https://getumbrel.com/',
|
||||
lightSrc: '/static/images/umbrell.png',
|
||||
darkSrc: '/static/images/umbrel.png'
|
||||
},
|
||||
{
|
||||
href: 'https://mynodebtc.com',
|
||||
lightSrc: '/static/images/mynodel.png',
|
||||
darkSrc: '/static/images/mynode.png'
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/shesek/spark-wallet',
|
||||
lightSrc: '/static/images/sparkl.png',
|
||||
darkSrc: '/static/images/spark.png'
|
||||
},
|
||||
{
|
||||
href: 'https://voltage.cloud',
|
||||
lightSrc: '/static/images/voltagel.png',
|
||||
darkSrc: '/static/images/voltage.png'
|
||||
},
|
||||
{
|
||||
href: 'https://breez.technology/sdk/',
|
||||
lightSrc: '/static/images/breezl.png',
|
||||
darkSrc: '/static/images/breez.png'
|
||||
},
|
||||
{
|
||||
href: 'https://blockstream.com/lightning/greenlight/',
|
||||
lightSrc: '/static/images/greenlightl.png',
|
||||
darkSrc: '/static/images/greenlight.png'
|
||||
},
|
||||
{
|
||||
href: 'https://getalby.com',
|
||||
lightSrc: '/static/images/albyl.png',
|
||||
darkSrc: '/static/images/alby.png'
|
||||
},
|
||||
{
|
||||
href: 'https://zbd.gg',
|
||||
lightSrc: '/static/images/zbdl.png',
|
||||
darkSrc: '/static/images/zbd.png'
|
||||
},
|
||||
{
|
||||
href: 'https://phoenix.acinq.co/server',
|
||||
lightSrc: '/static/images/phoenixdl.png',
|
||||
darkSrc: '/static/images/phoenixd.png'
|
||||
},
|
||||
{
|
||||
href: 'https://boltz.exchange/',
|
||||
lightSrc: '/static/images/boltzl.svg',
|
||||
darkSrc: '/static/images/boltz.svg'
|
||||
},
|
||||
{
|
||||
href: 'https://www.blink.sv/',
|
||||
lightSrc: '/static/images/blink_logol.png',
|
||||
darkSrc: '/static/images/blink_logo.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showLogos() {
|
||||
return (
|
||||
this.LNBITS_DENOMINATION == 'sats' &&
|
||||
this.SITE_TITLE == 'LNbits' &&
|
||||
this.LNBITS_SHOW_HOME_PAGE_ELEMENTS == true
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
window.app.component('lnbits-label-selector', {
|
||||
template: '#lnbits-label-selector',
|
||||
props: ['labels'],
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
labelFilter: '',
|
||||
localLabels: []
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleLabel(label) {
|
||||
const hasLabel = this.localLabels.includes(label.name)
|
||||
|
||||
if (hasLabel) {
|
||||
const index = this.localLabels.indexOf(label.name)
|
||||
if (index !== -1) {
|
||||
this.localLabels.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
this.localLabels.push(label.name)
|
||||
}
|
||||
},
|
||||
saveLabels() {
|
||||
this.$emit('update:labels', this.localLabels)
|
||||
},
|
||||
clearLabels() {
|
||||
this.localLabels = []
|
||||
this.saveLabels()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.localLabels = [...this.labels]
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
window.app.component('lnbits-language-dropdown', {
|
||||
template: '#lnbits-language-dropdown',
|
||||
mixins: [window.windowMixin],
|
||||
methods: {
|
||||
activeLanguage(lang) {
|
||||
return window.i18n.global.locale === lang
|
||||
},
|
||||
changeLanguage(newValue) {
|
||||
this.g.locale = newValue
|
||||
window.i18n.global.locale = newValue
|
||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
langs: [
|
||||
{value: 'en', label: 'English', display: '🇬🇧 EN'},
|
||||
{value: 'de', label: 'Deutsch', display: '🇩🇪 DE'},
|
||||
{value: 'es', label: 'Español', display: '🇪🇸 ES'},
|
||||
{value: 'jp', label: '日本語', display: '🇯🇵 JP'},
|
||||
{value: 'cn', label: '中文', display: '🇨🇳 CN'},
|
||||
{value: 'fr', label: 'Français', display: '🇫🇷 FR'},
|
||||
{value: 'it', label: 'Italiano', display: '🇮🇹 IT'},
|
||||
{value: 'pi', label: 'Pirate', display: '🏴☠️ PI'},
|
||||
{value: 'nl', label: 'Nederlands', display: '🇳🇱 NL'},
|
||||
{value: 'we', label: 'Cymraeg', display: '🏴 CY'},
|
||||
{value: 'pl', label: 'Polski', display: '🇵🇱 PL'},
|
||||
{value: 'pt', label: 'Português', display: '🇵🇹 PT'},
|
||||
{value: 'br', label: 'Português do Brasil', display: '🇧🇷 BR'},
|
||||
{value: 'cs', label: 'Česky', display: '🇨🇿 CS'},
|
||||
{value: 'sk', label: 'Slovensky', display: '🇸🇰 SK'},
|
||||
{value: 'kr', label: '한국어', display: '🇰🇷 KR'},
|
||||
{value: 'fi', label: 'Suomi', display: '🇫🇮 FI'}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
window.app.component('lnbits-manage-extension-list', {
|
||||
mixins: [window.windowMixin],
|
||||
template: '#lnbits-manage-extension-list',
|
||||
data() {
|
||||
return {
|
||||
extensions: [],
|
||||
userExtensions: [],
|
||||
searchTerm: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'g.user.extensions': {
|
||||
async handler() {
|
||||
await this.loadExtensions()
|
||||
}
|
||||
},
|
||||
searchTerm() {
|
||||
this.filterUserExtensionsByTerm()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
map(data) {
|
||||
const obj = {...data}
|
||||
obj.url = ['/', obj.code, '/'].join('')
|
||||
return obj
|
||||
},
|
||||
async loadExtensions() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request('GET', '/api/v1/extension')
|
||||
this.extensions = data
|
||||
.map(extension => this.map(extension))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
this.filterUserExtensionsByTerm()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
filterUserExtensionsByTerm() {
|
||||
const userExts = this.g.user.extensions
|
||||
this.userExtensions = this.extensions
|
||||
.filter(o => userExts.includes(o.code))
|
||||
.filter(o => {
|
||||
if (this.searchTerm === '') return true
|
||||
return `${o.code} ${o.name} ${o.short_description} ${o.url}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchTerm.toLocaleLowerCase())
|
||||
})
|
||||
.map(obj => {
|
||||
obj.isActive = window.location.pathname.startsWith(obj.url)
|
||||
return obj
|
||||
})
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.loadExtensions()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
window.app.component('lnbits-manage-wallet-list', {
|
||||
template: '#lnbits-manage-wallet-list',
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
walletName: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
+175
-56
@@ -1,7 +1,6 @@
|
||||
window.app.component('payment-list', {
|
||||
name: 'payment-list',
|
||||
template: '#payment-list',
|
||||
props: ['update', 'lazy', 'wallet'],
|
||||
window.app.component('lnbits-payment-list', {
|
||||
template: '#lnbits-payment-list',
|
||||
props: ['wallet', 'paymentFilter'],
|
||||
mixins: [window.windowMixin],
|
||||
data() {
|
||||
return {
|
||||
@@ -32,9 +31,6 @@ window.app.component('payment-list', {
|
||||
rowsNumber: 10
|
||||
},
|
||||
search: '',
|
||||
filter: {
|
||||
'status[ne]': 'failed'
|
||||
},
|
||||
loading: false
|
||||
},
|
||||
searchDate: {from: null, to: null},
|
||||
@@ -125,7 +121,9 @@ window.app.component('payment-list', {
|
||||
show: false,
|
||||
payment: null,
|
||||
preimage: null
|
||||
}
|
||||
},
|
||||
selectedPayment: null,
|
||||
filterLabels: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -139,7 +137,7 @@ window.app.component('payment-list', {
|
||||
return LNbits.utils.search(this.payments, q)
|
||||
},
|
||||
paymentsOmitter() {
|
||||
if (this.$q.screen.lt.md && this.mobileSimple) {
|
||||
if (this.$q.screen.lt.md && this.g.mobileSimple) {
|
||||
return this.payments.length > 0 ? [this.payments[0]] : []
|
||||
}
|
||||
return this.payments
|
||||
@@ -149,6 +147,49 @@ window.app.component('payment-list', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapPayment(data) {
|
||||
const obj = {
|
||||
checking_id: data.checking_id,
|
||||
status: data.status,
|
||||
amount: data.amount,
|
||||
fee: data.fee,
|
||||
memo: data.memo,
|
||||
time: data.time,
|
||||
bolt11: data.bolt11,
|
||||
preimage: data.preimage,
|
||||
payment_hash: data.payment_hash,
|
||||
expiry: data.expiry,
|
||||
extra: data.extra ?? {},
|
||||
wallet_id: data.wallet_id,
|
||||
webhook: data.webhook,
|
||||
webhook_status: data.webhook_status,
|
||||
fiat_amount: data.fiat_amount,
|
||||
fiat_currency: data.fiat_currency,
|
||||
labels: data.labels
|
||||
}
|
||||
obj.date = moment.utc(data.created_at).local().format(window.dateFormat)
|
||||
obj.dateFrom = moment.utc(data.created_at).local().fromNow()
|
||||
obj.expirydate = moment.utc(obj.expiry).local().format(window.dateFormat)
|
||||
obj.expirydateFrom = moment.utc(obj.expiry).local().fromNow()
|
||||
obj.msat = obj.amount
|
||||
obj.sat = obj.msat / 1000
|
||||
obj.tag = obj.extra?.tag
|
||||
obj.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
||||
obj.sat
|
||||
)
|
||||
obj.isIn = obj.amount > 0
|
||||
obj.isOut = obj.amount < 0
|
||||
obj.isPending = obj.status === 'pending'
|
||||
obj.isPaid = obj.status === 'success'
|
||||
obj.isFailed = obj.status === 'failed'
|
||||
obj._q = [obj.memo, obj.sat].join(' ').toLowerCase()
|
||||
try {
|
||||
obj.details = JSON.parse(data.extra?.details || '{}')
|
||||
} catch {
|
||||
obj.details = {extraDetails: data.extra?.details}
|
||||
}
|
||||
return obj
|
||||
},
|
||||
searchByDate() {
|
||||
if (typeof this.searchDate === 'string') {
|
||||
this.searchDate = {
|
||||
@@ -157,32 +198,48 @@ window.app.component('payment-list', {
|
||||
}
|
||||
}
|
||||
if (this.searchDate.from) {
|
||||
this.paymentsTable.filter['time[ge]'] =
|
||||
this.searchDate.from + 'T00:00:00'
|
||||
this.paymentFilter['time[ge]'] = this.searchDate.from + 'T00:00:00'
|
||||
}
|
||||
if (this.searchDate.to) {
|
||||
this.paymentsTable.filter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
||||
this.paymentFilter['time[le]'] = this.searchDate.to + 'T23:59:59'
|
||||
}
|
||||
|
||||
this.fetchPayments()
|
||||
},
|
||||
searchByLabels(labels) {
|
||||
if (!labels || labels.length === 0) {
|
||||
this.clearLabelSeach()
|
||||
return
|
||||
}
|
||||
this.filterLabels = labels
|
||||
this.paymentsTable.filter['labels[every]'] = labels
|
||||
this.fetchPayments()
|
||||
},
|
||||
clearDateSeach() {
|
||||
this.searchDate = {from: null, to: null}
|
||||
delete this.paymentsTable.filter['time[ge]']
|
||||
delete this.paymentsTable.filter['time[le]']
|
||||
delete this.paymentFilter['time[ge]']
|
||||
delete this.paymentFilter['time[le]']
|
||||
this.fetchPayments()
|
||||
},
|
||||
clearLabelSeach() {
|
||||
this.filterLabels = []
|
||||
delete this.paymentsTable.filter['labels[every]']
|
||||
this.fetchPayments()
|
||||
},
|
||||
fetchPayments(props) {
|
||||
this.$emit('filter-changed', {...this.paymentsTable.filter})
|
||||
const params = LNbits.utils.prepareFilterQuery(this.paymentsTable, props)
|
||||
const params = LNbits.utils.prepareFilterQuery(
|
||||
this.paymentsTable,
|
||||
props,
|
||||
this.paymentFilter
|
||||
)
|
||||
this.paymentsTable.loading = true
|
||||
return LNbits.api
|
||||
.getPayments(this.currentWallet, params)
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
this.recheckPendingPayments()
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
@@ -200,9 +257,7 @@ window.app.component('payment-list', {
|
||||
.then(response => {
|
||||
this.paymentsTable.loading = false
|
||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
this.payments = response.data.data.map(obj => {
|
||||
return LNbits.map.payment(obj)
|
||||
})
|
||||
this.payments = response.data.data.map(this.mapPayment)
|
||||
})
|
||||
.catch(err => {
|
||||
this.paymentsTable.loading = false
|
||||
@@ -229,6 +284,41 @@ window.app.component('payment-list', {
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
recheckPendingPayments() {
|
||||
const pendingPayments = this.payments.filter(p => p.status === 'pending')
|
||||
if (pendingPayments.length === 0) return
|
||||
|
||||
const params = [
|
||||
'recheck_pending=true',
|
||||
'checking_id[in]=' + pendingPayments.map(p => p.checking_id).join(',')
|
||||
].join('&')
|
||||
|
||||
LNbits.api
|
||||
.getPayments(this.currentWallet, params)
|
||||
.then(response => {
|
||||
let updatedPayments = 0
|
||||
response.data.data.forEach(updatedPayment => {
|
||||
if (updatedPayment.status !== 'pending') {
|
||||
const index = this.payments.findIndex(
|
||||
p => p.checking_id === updatedPayment.checking_id
|
||||
)
|
||||
if (index !== -1) {
|
||||
this.payments.splice(index, 1, this.mapPayment(updatedPayment))
|
||||
updatedPayments += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
if (updatedPayments > 0) {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('payment_successful')
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.warn(err)
|
||||
})
|
||||
},
|
||||
showHoldInvoiceDialog(payment) {
|
||||
this.hodlInvoice.show = true
|
||||
this.hodlInvoice.preimage = ''
|
||||
@@ -272,7 +362,7 @@ window.app.component('payment-list', {
|
||||
}
|
||||
const params = new URLSearchParams(query)
|
||||
LNbits.api.getPayments(this.g.wallet, params).then(response => {
|
||||
let payments = response.data.data.map(LNbits.map.payment)
|
||||
let payments = response.data.data.map(this.mapPayment)
|
||||
let columns = this.paymentsCSV.columns
|
||||
|
||||
if (detailed) {
|
||||
@@ -327,33 +417,74 @@ window.app.component('payment-list', {
|
||||
},
|
||||
handleFilterChanged() {
|
||||
const {success, pending, failed, incoming, outgoing} = this.searchStatus
|
||||
|
||||
delete this.paymentsTable.filter['status[ne]']
|
||||
delete this.paymentsTable.filter['status[eq]']
|
||||
let paymentFilter = this.paymentFilter || {}
|
||||
delete paymentFilter['status[ne]']
|
||||
delete paymentFilter['status[eq]']
|
||||
if (success && pending && failed) {
|
||||
// No status filter
|
||||
} else if (success && pending) {
|
||||
this.paymentsTable.filter['status[ne]'] = 'failed'
|
||||
paymentFilter['status[ne]'] = 'failed'
|
||||
} else if (success && failed) {
|
||||
this.paymentsTable.filter['status[ne]'] = 'pending'
|
||||
paymentFilter['status[ne]'] = 'pending'
|
||||
} else if (failed && pending) {
|
||||
this.paymentsTable.filter['status[ne]'] = 'success'
|
||||
} else if (success) {
|
||||
this.paymentsTable.filter['status[eq]'] = 'success'
|
||||
} else if (pending) {
|
||||
this.paymentsTable.filter['status[eq]'] = 'pending'
|
||||
} else if (failed) {
|
||||
this.paymentsTable.filter['status[eq]'] = 'failed'
|
||||
paymentFilter['status[ne]'] = 'success'
|
||||
} else if (success && !pending && !failed) {
|
||||
paymentFilter['status[eq]'] = 'success'
|
||||
} else if (pending && !success && !failed) {
|
||||
paymentFilter['status[eq]'] = 'pending'
|
||||
} else if (failed && !success && !pending) {
|
||||
paymentFilter['status[eq]'] = 'failed'
|
||||
}
|
||||
|
||||
delete this.paymentsTable.filter['amount[ge]']
|
||||
delete this.paymentsTable.filter['amount[le]']
|
||||
if (incoming && outgoing) {
|
||||
delete paymentFilter['amount[ge]']
|
||||
delete paymentFilter['amount[le]']
|
||||
if ((incoming && outgoing) || (!incoming && !outgoing)) {
|
||||
// do nothing
|
||||
} else if (incoming) {
|
||||
this.paymentsTable.filter['amount[ge]'] = 0
|
||||
} else if (outgoing) {
|
||||
this.paymentsTable.filter['amount[le]'] = 0
|
||||
} else if (incoming && !outgoing) {
|
||||
paymentFilter['amount[ge]'] = 0
|
||||
} else if (outgoing && !incoming) {
|
||||
paymentFilter['amount[le]'] = 0
|
||||
}
|
||||
this.paymentFilter = paymentFilter
|
||||
},
|
||||
async savePaymentLabels(labels) {
|
||||
if (!this.selectedPayment) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'No payment selected'
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await LNbits.api.request(
|
||||
'PUT',
|
||||
`/api/v1/payments/${this.selectedPayment.payment_hash}/labels`,
|
||||
this.g.wallet.adminkey,
|
||||
{
|
||||
labels: labels
|
||||
}
|
||||
)
|
||||
|
||||
const payment = this.payments.find(
|
||||
p => p.checking_id === this.selectedPayment.checking_id
|
||||
)
|
||||
if (payment) {
|
||||
payment.labels = [...labels]
|
||||
}
|
||||
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: this.$t('payment_labels_updated')
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
isLightColor(color) {
|
||||
try {
|
||||
return Quasar.colors.luminosity(color) > 0.5
|
||||
} catch (e) {
|
||||
console.warning(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -367,23 +498,11 @@ window.app.component('payment-list', {
|
||||
this.fetchPayments()
|
||||
}
|
||||
},
|
||||
lazy(newVal) {
|
||||
if (newVal === true) this.fetchPayments()
|
||||
},
|
||||
update() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.updatePayments'() {
|
||||
this.fetchPayments()
|
||||
},
|
||||
'g.wallet': {
|
||||
handler(newWallet) {
|
||||
this.fetchPayments()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.lazy === undefined) this.fetchPayments()
|
||||
this.fetchPayments()
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user