Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3535e3965 | ||
|
|
f405d6834a | ||
|
|
93bcd25ab7 | ||
|
|
9d288e6a98 | ||
|
|
b71382aa1e | ||
|
|
241f1f81a8 |
@@ -5,9 +5,8 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</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)
|
 [![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" />
|
<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
|
# LNbits — The most powerful Bitcoin & Lightning toolkit
|
||||||
|
|
||||||
@@ -78,11 +77,13 @@ 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.
|
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://shop.lnbits.com/)
|
[](https://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
[](https://extensions.lnbits.com/) [](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
[](https://extensions.lnbits.com/)
|
||||||
|
|
||||||
[docs]: https://github.com/lnbits/lnbits/wiki
|
[docs]: https://github.com/lnbits/lnbits/wiki
|
||||||
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
[docs-badge]: https://img.shields.io/badge/docs-lnbits.org-673ab7.svg
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ color_scheme: dark
|
|||||||
logo: "/logos/lnbits-full-inverse.png"
|
logo: "/logos/lnbits-full-inverse.png"
|
||||||
search_enabled: true
|
search_enabled: true
|
||||||
url: https://docs.lnbits.org
|
url: https://docs.lnbits.org
|
||||||
markdown: gfm
|
|
||||||
aux_links:
|
aux_links:
|
||||||
"LNbits on GitHub":
|
"LNbits on GitHub":
|
||||||
- "//github.com/lnbits/lnbits"
|
- "//github.com/lnbits/lnbits"
|
||||||
|
|||||||
+22
-34
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: Admin UI
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -18,13 +12,11 @@ nav_order: 1
|
|||||||
|
|
||||||
# LNBits Admin UI
|
# LNBits Admin UI
|
||||||
|
|
||||||
[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.
|
||||||
|
|
||||||
**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.
|
<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)**.
|
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" />
|
<img width="900" height="640" alt="grafik" src="https://github.com/user-attachments/assets/d8852b4b-21be-446f-a1e7-d3eb794d3505" />
|
||||||
@@ -44,27 +36,6 @@ For a complete reference of legacy variables consult **[.env.example](../../.env
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> See **[Super User](./super_user.md)** for the role and permission differences compared to Admin Users.
|
> 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
|
## Enabling or disabling the Admin UI
|
||||||
|
|
||||||
The Admin UI is enabled by default on new installs. To change the state:
|
The Admin UI is enabled by default on new installs. To change the state:
|
||||||
@@ -105,6 +76,23 @@ 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.
|
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
|
## 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.
|
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.
|
||||||
@@ -117,8 +105,8 @@ When set **at least one**, LNbits becomes private: only the listed users and Adm
|
|||||||
## Additional Guides
|
## Additional Guides
|
||||||
|
|
||||||
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
- **[Backend Wallets](./wallets.md)** — Explore options to fund your LNbits instance.
|
||||||
- **[User Roles](./user_roles.md)** — Overview of existing roles in LNbits.
|
- **[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.
|
- **[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.
|
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
@@ -127,7 +115,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)
|
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://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: Extension Install
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
# Extension Install
|
# 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.
|
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,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: FastAPI extension upgrade
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
## Defining a route with path parameters
|
## Defining a route with path parameters
|
||||||
|
|
||||||
**old:**
|
**old:**
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: Wallet comparison
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -78,7 +72,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)
|
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://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
+27
-33
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: Installation
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -57,8 +51,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.
|
- LNbits will create a folder for DB and extension files **in the same directory** as the AppImage.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Option 2: UV (recommended for developers)
|
## Option 2: UV (recommended for developers)
|
||||||
@@ -123,8 +117,8 @@ uv run lnbits
|
|||||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
## Option 2a (Legacy): Poetry — _Replaced by UV_
|
||||||
@@ -203,8 +197,8 @@ poetry run lnbits
|
|||||||
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
#### Use Admin UI → Extensions → "Update All" to bring extensions up to the proper level
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -223,8 +217,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).
|
- You can use `./lnbits.sh` to run, but for more control: `cd lnbits` and use `uv run lnbits` (see Option 2).
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -292,7 +286,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)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -346,8 +340,8 @@ docker build --build-arg POETRY_INSTALL_ARGS="-E breez" -t lnbits/lnbits .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNBits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -386,14 +380,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.
|
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`.
|
> 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
|
> 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`, then in `fly.toml` use
|
||||||
> `LNBITS_ENDPOINT="https://demo.lnbits.com"`.
|
> `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.
|
> 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):
|
> Example (LND REST funding source):
|
||||||
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
> `fly secrets set LND_REST_MACAROON=<hex_macaroon_data>`
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -451,8 +445,8 @@ poetry add setuptools wheel
|
|||||||
|
|
||||||
> 
|
> 
|
||||||
>
|
>
|
||||||
> **Next steps**
|
> **Next steps**
|
||||||
> Install complete → **[Running LNbits](#run-the-server)**
|
> Install complete → **[Running LNbits](#run-the-server)**
|
||||||
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
> Update LNbits → **[Update LNbits (all methods)](#update-lnbits-all-methods)**
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@@ -530,22 +524,22 @@ Visit **[http://localhost:5000/](http://localhost:5000/)** (or `0.0.0.0:5000`).
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Paths overview**
|
> **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`
|
> Example: `~/lnbits/data/.super_user` • View: `cat ~/lnbits/data/.super_user`
|
||||||
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
> - **Environment file:** `<lnbits_root>/.env` (for bare-metal installs)
|
||||||
> - **Docker:** bind a host directory to `/app/data`.
|
> - **Docker:** bind a host directory to `/app/data`.
|
||||||
> On the host the SuperUser file is at `<host_data_dir>/.super_user`.
|
> 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).
|
> The container reads `/app/.env` (usually bind-mounted from your project root).
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **Local Lightning test network**
|
> **Local Lightning test network**
|
||||||
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
> Use **Polar** to spin up a safe local Lightning environment and test LNbits without touching your live setup.
|
||||||
> https://lightningpolar.com/
|
> https://lightningpolar.com/
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> **API comparison before updates**
|
> **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.
|
> 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.
|
> Crafted by [Arbadacarbayk](https://github.com/arbadacarbaYK) - a standout contribution that makes pre-release reviews fast and reliable.
|
||||||
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
> https://arbadacarbayk.github.io/LNbits_TableTown/
|
||||||
|
|
||||||
# Additional guides
|
# Additional guides
|
||||||
@@ -890,5 +884,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)
|
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/)
|
[](https://shop.lnbits.com/) [](https://my.lnbits.com/login) [](https://news.lnbits.com/) [](https://extensions.lnbits.com/)
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: Super user
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -116,8 +110,8 @@ These are practical tips for running a safe and friendly instance.
|
|||||||
## Additional guides
|
## Additional guides
|
||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings in the browser instead of editing `.env` or using the CLI for routine tasks.
|
- **[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.
|
- **[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.
|
- **[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.
|
- **[Install LNBits](./installation.md)** — Choose your prefared way to install LNBits.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
title: User Roles
|
|
||||||
nav_order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
|
||||||
@@ -20,7 +14,7 @@ nav_order: 1
|
|||||||
|
|
||||||
### Understand **who can do what** in seconds: `Super User`, `Admin`, and `Regular User`.
|
### 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) •
|
[Roles at a Glance](#roles-at-a-glance) •
|
||||||
[Super User](#super-user--master-control) •
|
[Super User](#super-user--master-control) •
|
||||||
[Admin](#admin--day-to-day-manager) •
|
[Admin](#admin--day-to-day-manager) •
|
||||||
@@ -87,7 +81,7 @@ For using LNbits, not administering it.
|
|||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
- **[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.
|
- **[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
|
## Powered by LNbits
|
||||||
|
|
||||||
@@ -95,7 +89,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)
|
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://shop.lnbits.com/)
|
||||||
[](https://my.lnbits.com/login)
|
[](https://my.lnbits.com/login)
|
||||||
[](https://news.lnbits.com/)
|
[](https://news.lnbits.com/)
|
||||||
|
|||||||
@@ -354,8 +354,8 @@ Get more info here [https://docs.strike.me/strike-oauth-connect/](https://docs.s
|
|||||||
## Additional Guides
|
## Additional Guides
|
||||||
|
|
||||||
- **[Admin UI](./admin_ui.md)** — Manage server settings via a clean UI (avoid editing `.env` by hand).
|
- **[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.
|
- **[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.
|
- **[Funding sources](./funding-sources_table.md)** — What’s available and how to enable/configure each.
|
||||||
|
|
||||||
## Powered by LNbits
|
## Powered by LNbits
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from fastapi import APIRouter, FastAPI
|
|||||||
from .db import core_app_extra, db
|
from .db import core_app_extra, db
|
||||||
from .views.admin_api import admin_router
|
from .views.admin_api import admin_router
|
||||||
from .views.api import api_router
|
from .views.api import api_router
|
||||||
from .views.asset_api import asset_router
|
|
||||||
from .views.audit_api import audit_router
|
from .views.audit_api import audit_router
|
||||||
from .views.auth_api import auth_router
|
from .views.auth_api import auth_router
|
||||||
from .views.callback_api import callback_router
|
from .views.callback_api import callback_router
|
||||||
@@ -45,7 +44,6 @@ def init_core_routers(app: FastAPI):
|
|||||||
app.include_router(webpush_router)
|
app.include_router(webpush_router)
|
||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(audit_router)
|
app.include_router(audit_router)
|
||||||
app.include_router(asset_router)
|
|
||||||
app.include_router(fiat_router)
|
app.include_router(fiat_router)
|
||||||
app.include_router(lnurl_router)
|
app.include_router(lnurl_router)
|
||||||
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
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,12 +16,11 @@ async def get_audit_entries(
|
|||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Page[AuditEntry]:
|
) -> Page[AuditEntry]:
|
||||||
return await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"SELECT * FROM audit",
|
"SELECT * from audit",
|
||||||
[],
|
[],
|
||||||
{},
|
{},
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=AuditEntry,
|
model=AuditEntry,
|
||||||
table_name="audit",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async def get_standalone_payment(
|
|||||||
) -> Payment | None:
|
) -> Payment | None:
|
||||||
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
clause: str = "checking_id = :checking_id OR payment_hash = :hash"
|
||||||
values = {
|
values = {
|
||||||
|
"wallet_id": wallet_id,
|
||||||
"checking_id": checking_id_or_hash,
|
"checking_id": checking_id_or_hash,
|
||||||
"hash": checking_id_or_hash,
|
"hash": checking_id_or_hash,
|
||||||
}
|
}
|
||||||
@@ -46,10 +47,6 @@ async def get_standalone_payment(
|
|||||||
clause = f"({clause}) AND amount > 0"
|
clause = f"({clause}) AND amount > 0"
|
||||||
|
|
||||||
if wallet_id:
|
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"
|
clause = f"({clause}) AND wallet_id = :wallet_id"
|
||||||
|
|
||||||
row = await (conn or db).fetchone(
|
row = await (conn or db).fetchone(
|
||||||
@@ -69,16 +66,13 @@ async def get_standalone_payment(
|
|||||||
async def get_wallet_payment(
|
async def get_wallet_payment(
|
||||||
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
wallet_id: str, payment_hash: str, conn: Connection | None = None
|
||||||
) -> Payment | 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(
|
payment = await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM apipayments
|
FROM apipayments
|
||||||
WHERE wallet_id = :wallet AND payment_hash = :hash
|
WHERE wallet_id = :wallet AND payment_hash = :hash
|
||||||
""",
|
""",
|
||||||
{"wallet": wallet.source_wallet_id, "hash": payment_hash},
|
{"wallet": wallet_id, "hash": payment_hash},
|
||||||
Payment,
|
Payment,
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
@@ -134,11 +128,7 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
clause.append(f"time > {db.timestamp_placeholder('time')}")
|
||||||
|
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
wallet = await get_wallet(wallet_id)
|
values["wallet_id"] = 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")
|
clause.append("wallet_id = :wallet_id")
|
||||||
elif user_id:
|
elif user_id:
|
||||||
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
only_user_wallets = await _only_user_wallets_statement(user_id, conn=conn)
|
||||||
@@ -181,7 +171,6 @@ async def get_payments_paginated( # noqa: C901
|
|||||||
values,
|
values,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=Payment,
|
model=Payment,
|
||||||
table_name="apipayments",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -293,7 +282,6 @@ async def create_payment(
|
|||||||
fee=-abs(data.fee),
|
fee=-abs(data.fee),
|
||||||
tag=extra.get("tag", None),
|
tag=extra.get("tag", None),
|
||||||
extra=extra,
|
extra=extra,
|
||||||
labels=data.labels or [],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("apipayments", payment)
|
await (conn or db).insert("apipayments", payment)
|
||||||
@@ -332,7 +320,7 @@ async def get_payments_history(
|
|||||||
|
|
||||||
date_trunc = db.datetime_grouping(group)
|
date_trunc = db.datetime_grouping(group)
|
||||||
|
|
||||||
values: dict[str, Any] = {
|
values = {
|
||||||
"wallet_id": wallet_id,
|
"wallet_id": wallet_id,
|
||||||
}
|
}
|
||||||
# count outgoing payments if they are still pending
|
# count outgoing payments if they are still pending
|
||||||
@@ -362,10 +350,10 @@ async def get_payments_history(
|
|||||||
)
|
)
|
||||||
if wallet_id:
|
if wallet_id:
|
||||||
wallet = await get_wallet(wallet_id)
|
wallet = await get_wallet(wallet_id)
|
||||||
if not wallet or not wallet.can_view_payments:
|
if wallet:
|
||||||
return []
|
balance = wallet.balance_msat
|
||||||
balance = wallet.balance_msat
|
else:
|
||||||
values["wallet_id"] = wallet.source_wallet_id
|
raise ValueError("Unknown wallet")
|
||||||
else:
|
else:
|
||||||
balance = await get_total_balance()
|
balance = await get_total_balance()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Any
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
from lnbits.core.crud.extensions import get_user_active_extensions_ids
|
||||||
from lnbits.core.crud.wallets import create_wallet, get_wallets
|
from lnbits.core.crud.wallets import get_wallets
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import UserAcls
|
from lnbits.core.models import UserAcls
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
@@ -89,7 +89,6 @@ async def get_accounts(
|
|||||||
filters=filters,
|
filters=filters,
|
||||||
model=AccountOverview,
|
model=AccountOverview,
|
||||||
group_by=["accounts.id"],
|
group_by=["accounts.id"],
|
||||||
table_name="accounts",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,13 +179,8 @@ async def get_user(user_id: str, conn: Connection | None = None) -> User | None:
|
|||||||
async def get_user_from_account(
|
async def get_user_from_account(
|
||||||
account: Account, conn: Connection | None = None
|
account: Account, conn: Connection | None = None
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
extensions = await get_user_active_extensions_ids(account.id, conn=conn)
|
extensions = await get_user_active_extensions_ids(account.id, conn)
|
||||||
wallets = await get_wallets(account.id, deleted=False, conn=conn)
|
wallets = await get_wallets(account.id, False, conn=conn)
|
||||||
|
|
||||||
if len(wallets) == 0:
|
|
||||||
wallet = await create_wallet(user_id=account.id, conn=conn)
|
|
||||||
wallets.append(wallet)
|
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
id=account.id,
|
id=account.id,
|
||||||
email=account.email,
|
email=account.email,
|
||||||
|
|||||||
+11
-79
@@ -3,7 +3,7 @@ from time import time
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.wallets import WalletsFilters, WalletType
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
from lnbits.db import Connection, Filters, Page
|
from lnbits.db import Connection, Filters, Page
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
@@ -14,22 +14,17 @@ async def create_wallet(
|
|||||||
*,
|
*,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
wallet_name: str | None = None,
|
wallet_name: str | None = None,
|
||||||
wallet_type: WalletType = WalletType.LIGHTNING,
|
|
||||||
shared_wallet_id: str | None = None,
|
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
wallet_id = uuid4().hex
|
wallet_id = uuid4().hex
|
||||||
wallet = Wallet(
|
wallet = Wallet(
|
||||||
id=wallet_id,
|
id=wallet_id,
|
||||||
name=wallet_name or settings.lnbits_default_wallet_name,
|
name=wallet_name or settings.lnbits_default_wallet_name,
|
||||||
wallet_type=wallet_type.value,
|
|
||||||
shared_wallet_id=shared_wallet_id,
|
|
||||||
user=user_id,
|
user=user_id,
|
||||||
adminkey=uuid4().hex,
|
adminkey=uuid4().hex,
|
||||||
inkey=uuid4().hex,
|
inkey=uuid4().hex,
|
||||||
currency=settings.lnbits_default_accounting_currency or "USD",
|
currency=settings.lnbits_default_accounting_currency or "USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
await (conn or db).insert("wallets", wallet)
|
await (conn or db).insert("wallets", wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
@@ -108,7 +103,7 @@ async def delete_unused_wallets(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_standalone_wallet(
|
async def get_wallet(
|
||||||
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
wallet_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
query = """
|
query = """
|
||||||
@@ -126,23 +121,8 @@ async def get_standalone_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(
|
async def get_wallets(
|
||||||
user_id: str,
|
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
deleted: bool | None = False,
|
|
||||||
wallet_type: WalletType | None = None,
|
|
||||||
conn: Connection | None = None,
|
|
||||||
) -> list[Wallet]:
|
) -> list[Wallet]:
|
||||||
query = """
|
query = """
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
@@ -152,20 +132,12 @@ async def get_wallets(
|
|||||||
"""
|
"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += " AND deleted = :deleted "
|
query += " AND deleted = :deleted "
|
||||||
if wallet_type is not None:
|
return await (conn or db).fetchall(
|
||||||
query += " AND wallet_type = :wallet_type "
|
|
||||||
wallets = await (conn or db).fetchall(
|
|
||||||
query,
|
query,
|
||||||
{
|
{"user": user_id, "deleted": deleted},
|
||||||
"user": user_id,
|
|
||||||
"deleted": deleted,
|
|
||||||
"wallet_type": wallet_type.value if wallet_type else None,
|
|
||||||
},
|
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_source_wallets(wallets, conn)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_paginated(
|
async def get_wallets_paginated(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
@@ -177,7 +149,7 @@ async def get_wallets_paginated(
|
|||||||
deleted = False
|
deleted = False
|
||||||
|
|
||||||
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
where: list[str] = [""" "user" = :user AND deleted = :deleted """]
|
||||||
wallets = await (conn or db).fetch_page(
|
return await (conn or db).fetch_page(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
@@ -187,27 +159,20 @@ async def get_wallets_paginated(
|
|||||||
values={"user": user_id, "deleted": deleted},
|
values={"user": user_id, "deleted": deleted},
|
||||||
filters=filters,
|
filters=filters,
|
||||||
model=Wallet,
|
model=Wallet,
|
||||||
table_name="wallets",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
wallets.data = await get_source_wallets(wallets.data, conn)
|
|
||||||
return wallets
|
|
||||||
|
|
||||||
|
|
||||||
async def get_wallets_ids(
|
async def get_wallets_ids(
|
||||||
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
user_id: str, deleted: bool | None = False, conn: Connection | None = None
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
query = """SELECT * FROM wallets WHERE "user" = :user"""
|
query = """SELECT id FROM wallets WHERE "user" = :user"""
|
||||||
if deleted is not None:
|
if deleted is not None:
|
||||||
query += " AND deleted = :deleted "
|
query += " AND deleted = :deleted"
|
||||||
wallets = await (conn or db).fetchall(
|
result: list[dict] = await (conn or db).fetchall(
|
||||||
query,
|
query,
|
||||||
{"user": user_id, "deleted": deleted},
|
{"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():
|
async def get_wallets_count():
|
||||||
@@ -220,7 +185,7 @@ async def get_wallet_for_key(
|
|||||||
key: str,
|
key: str,
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Wallet | None:
|
) -> Wallet | None:
|
||||||
wallet = await (conn or db).fetchone(
|
return await (conn or db).fetchone(
|
||||||
"""
|
"""
|
||||||
SELECT *, COALESCE((
|
SELECT *, COALESCE((
|
||||||
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
SELECT balance FROM balances WHERE wallet_id = wallets.id
|
||||||
@@ -231,39 +196,6 @@ async def get_wallet_for_key(
|
|||||||
{"key": key},
|
{"key": key},
|
||||||
Wallet,
|
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):
|
async def get_total_balance(conn: Connection | None = None):
|
||||||
|
|||||||
@@ -743,46 +743,3 @@ async def m034_add_stored_paylinks_to_wallet(db: Connection):
|
|||||||
ALTER TABLE wallets ADD COLUMN stored_paylinks TEXT
|
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
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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,7 +59,6 @@ class CreatePayment(BaseModel):
|
|||||||
expiry: datetime | None = None
|
expiry: datetime | None = None
|
||||||
webhook: str | None = None
|
webhook: str | None = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
labels: list[str] | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Payment(BaseModel):
|
class Payment(BaseModel):
|
||||||
@@ -82,7 +81,6 @@ class Payment(BaseModel):
|
|||||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
created_at: 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))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
labels: list[str] = []
|
|
||||||
extra: dict = {}
|
extra: dict = {}
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
@@ -186,15 +184,7 @@ class Payment(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentFilters(FilterModel):
|
class PaymentFilters(FilterModel):
|
||||||
__search_fields__ = [
|
__search_fields__ = ["memo", "amount", "wallet_id", "tag", "status", "time"]
|
||||||
"memo",
|
|
||||||
"amount",
|
|
||||||
"wallet_id",
|
|
||||||
"tag",
|
|
||||||
"status",
|
|
||||||
"time",
|
|
||||||
"labels",
|
|
||||||
]
|
|
||||||
|
|
||||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||||
|
|
||||||
@@ -208,7 +198,6 @@ class PaymentFilters(FilterModel):
|
|||||||
preimage: str | None
|
preimage: str | None
|
||||||
payment_hash: str | None
|
payment_hash: str | None
|
||||||
wallet_id: str | None
|
wallet_id: str | None
|
||||||
labels: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentDataPoint(BaseModel):
|
class PaymentDataPoint(BaseModel):
|
||||||
@@ -283,7 +272,6 @@ class CreateInvoice(BaseModel):
|
|||||||
bolt11: str | None = None
|
bolt11: str | None = None
|
||||||
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
lnurl_withdraw: LnurlWithdrawResponse | None = None
|
||||||
fiat_provider: str | None = None
|
fiat_provider: str | None = None
|
||||||
labels: list[str] = []
|
|
||||||
|
|
||||||
@validator("payment_hash")
|
@validator("payment_hash")
|
||||||
def check_hex(cls, v):
|
def check_hex(cls, v):
|
||||||
@@ -332,7 +320,3 @@ class CancelInvoice(BaseModel):
|
|||||||
def check_hex(cls, v):
|
def check_hex(cls, v):
|
||||||
_ = bytes.fromhex(v)
|
_ = bytes.fromhex(v)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class UpdatePaymentLabels(BaseModel):
|
|
||||||
labels: list[str] = []
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from lnbits.db import FilterModel
|
|||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
is_valid_email_address,
|
is_valid_email_address,
|
||||||
is_valid_external_id,
|
is_valid_external_id,
|
||||||
is_valid_label,
|
|
||||||
is_valid_pubkey,
|
is_valid_pubkey,
|
||||||
is_valid_username,
|
is_valid_username,
|
||||||
)
|
)
|
||||||
@@ -30,21 +29,6 @@ class UserNotifications(BaseModel):
|
|||||||
incoming_payments_sats: int = 0
|
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):
|
class UserExtra(BaseModel):
|
||||||
email_verified: bool | None = False
|
email_verified: bool | None = False
|
||||||
first_name: str | None = None
|
first_name: str | None = None
|
||||||
@@ -62,55 +46,6 @@ class UserExtra(BaseModel):
|
|||||||
|
|
||||||
notifications: UserNotifications = UserNotifications()
|
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):
|
class EndpointAccess(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
@@ -225,8 +160,6 @@ class Account(BaseModel):
|
|||||||
if user_uuid4.hex != self.id:
|
if user_uuid4.hex != self.id:
|
||||||
raise ValueError("User ID is not valid UUID4 hex string.")
|
raise ValueError("User ID is not valid UUID4 hex string.")
|
||||||
|
|
||||||
self.extra.validate_labels()
|
|
||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: int | None = 0
|
transaction_count: int | None = 0
|
||||||
@@ -237,7 +170,7 @@ class AccountOverview(Account):
|
|||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
__search_fields__ = [
|
__search_fields__ = [
|
||||||
"id",
|
"user",
|
||||||
"email",
|
"email",
|
||||||
"username",
|
"username",
|
||||||
"pubkey",
|
"pubkey",
|
||||||
@@ -245,18 +178,17 @@ class AccountFilters(FilterModel):
|
|||||||
"wallet_id",
|
"wallet_id",
|
||||||
]
|
]
|
||||||
__sort_fields__ = [
|
__sort_fields__ = [
|
||||||
"id",
|
"balance_msat",
|
||||||
"email",
|
"email",
|
||||||
"username",
|
"username",
|
||||||
"pubkey",
|
"transaction_count",
|
||||||
"external_id",
|
"wallet_count",
|
||||||
"created_at",
|
"last_payment",
|
||||||
"updated_at",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
id: str | None = None
|
|
||||||
username: str | None = None
|
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
|
user: str | None = None
|
||||||
|
username: str | None = None
|
||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
external_id: str | None = None
|
external_id: str | None = None
|
||||||
wallet_id: str | None = None
|
wallet_id: str | None = None
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from lnurl import encode as lnurl_encode
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from lnbits.core.models.lnurl import StoredPayLinks
|
from lnbits.core.models.lnurl import StoredPayLinks
|
||||||
from lnbits.db import FilterModel
|
from lnbits.db import FilterModel
|
||||||
|
from lnbits.helpers import url_for
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
|
||||||
@@ -19,95 +21,10 @@ class BaseWallet(BaseModel):
|
|||||||
balance_msat: int
|
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):
|
class WalletExtra(BaseModel):
|
||||||
icon: str = "flash_on"
|
icon: str = "flash_on"
|
||||||
color: str = "primary"
|
color: str = "primary"
|
||||||
pinned: bool = False
|
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):
|
class Wallet(BaseModel):
|
||||||
@@ -116,9 +33,6 @@ class Wallet(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
adminkey: str
|
adminkey: str
|
||||||
inkey: 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
|
deleted: bool = False
|
||||||
created_at: 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))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -126,65 +40,6 @@ class Wallet(BaseModel):
|
|||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
stored_paylinks: StoredPayLinks = StoredPayLinks()
|
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
|
@property
|
||||||
def balance(self) -> int:
|
def balance(self) -> int:
|
||||||
@@ -195,23 +50,16 @@ class Wallet(BaseModel):
|
|||||||
return self.balance_msat - settings.fee_reserve(self.balance_msat)
|
return self.balance_msat - settings.fee_reserve(self.balance_msat)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_lightning_wallet(self) -> bool:
|
def lnurlwithdraw_full(self) -> str:
|
||||||
return self.wallet_type == WalletType.LIGHTNING.value
|
url = url_for("/withdraw", external=True, usr=self.user, wal=self.id)
|
||||||
|
try:
|
||||||
@property
|
return lnurl_encode(url)
|
||||||
def is_lightning_shared_wallet(self) -> bool:
|
except Exception:
|
||||||
return self.wallet_type == WalletType.LIGHTNING_SHARED.value
|
return ""
|
||||||
|
|
||||||
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):
|
class CreateWallet(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
wallet_type: WalletType = WalletType.LIGHTNING
|
|
||||||
shared_wallet_id: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
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,8 +70,7 @@ async def check_balance_delta_changed():
|
|||||||
if settings.latest_balance_delta_sats is None:
|
if settings.latest_balance_delta_sats is None:
|
||||||
settings.latest_balance_delta_sats = status.delta_sats
|
settings.latest_balance_delta_sats = status.delta_sats
|
||||||
return
|
return
|
||||||
delta_change = abs(status.delta_sats - settings.latest_balance_delta_sats)
|
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||||
if delta_change >= settings.notification_balance_delta_threshold_sats:
|
|
||||||
enqueue_admin_notification(
|
enqueue_admin_notification(
|
||||||
NotificationType.balance_delta,
|
NotificationType.balance_delta,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from lnbits.core.crud import (
|
|||||||
mark_webhook_sent,
|
mark_webhook_sent,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_user
|
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 import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
@@ -241,10 +240,6 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
async with httpx.AsyncClient(headers=headers) as client:
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
try:
|
try:
|
||||||
check_callback_url(payment.webhook)
|
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 = await client.post(payment.webhook, json=payment.json(), timeout=40)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
await mark_webhook_sent(payment.payment_hash, str(r.status_code))
|
||||||
@@ -262,12 +257,6 @@ async def dispatch_webhook(payment: Payment):
|
|||||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
try:
|
try:
|
||||||
await send_ws_payment_notification(wallet, payment)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error sending websocket payment notification {e!s}")
|
logger.error(f"Error sending websocket payment notification {e!s}")
|
||||||
try:
|
try:
|
||||||
@@ -392,7 +381,7 @@ def _is_message_type_enabled(message_type: NotificationType) -> bool:
|
|||||||
if message_type == NotificationType.watchdog_check:
|
if message_type == NotificationType.watchdog_check:
|
||||||
return settings.lnbits_notification_watchdog
|
return settings.lnbits_notification_watchdog
|
||||||
if message_type == NotificationType.balance_delta:
|
if message_type == NotificationType.balance_delta:
|
||||||
return settings.notification_balance_delta_threshold_sats > 0
|
return settings.notification_balance_delta_changed
|
||||||
if message_type == NotificationType.server_start_stop:
|
if message_type == NotificationType.server_start_stop:
|
||||||
return settings.lnbits_notification_server_start_stop
|
return settings.lnbits_notification_server_start_stop
|
||||||
if message_type == NotificationType.server_status:
|
if message_type == NotificationType.server_status:
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ async def pay_invoice(
|
|||||||
extra: dict | None = None,
|
extra: dict | None = None,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
tag: str = "",
|
tag: str = "",
|
||||||
labels: list[str] | None = None,
|
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if settings.lnbits_only_allow_incoming_payments:
|
if settings.lnbits_only_allow_incoming_payments:
|
||||||
@@ -73,11 +72,6 @@ async def pay_invoice(
|
|||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
amount_msat = invoice.amount_msat
|
amount_msat = invoice.amount_msat
|
||||||
wallet = await _check_wallet_for_payment(wallet_id, tag, amount_msat, new_conn)
|
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):
|
if await is_internal_status_success(invoice.payment_hash, new_conn):
|
||||||
raise PaymentError("Internal invoice already paid.", status="failed")
|
raise PaymentError("Internal invoice already paid.", status="failed")
|
||||||
@@ -85,17 +79,16 @@ async def pay_invoice(
|
|||||||
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
_, extra = await calculate_fiat_amounts(amount_msat / 1000, wallet, extra=extra)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=wallet.source_wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=payment_request,
|
bolt11=payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
amount_msat=-amount_msat,
|
amount_msat=-amount_msat,
|
||||||
expiry=invoice.expiry_date,
|
expiry=invoice.expiry_date,
|
||||||
memo=description or invoice.description or "",
|
memo=description or invoice.description or "",
|
||||||
extra=extra,
|
extra=extra,
|
||||||
labels=labels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await _pay_invoice(wallet.source_wallet_id, create_payment_model, conn)
|
payment = await _pay_invoice(wallet.id, create_payment_model, conn)
|
||||||
|
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
|
||||||
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
await _credit_service_fee_wallet(wallet, payment, new_conn)
|
||||||
@@ -209,7 +202,6 @@ async def create_wallet_invoice(wallet_id: str, data: CreateInvoice) -> Payment:
|
|||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
payment_hash=data.payment_hash,
|
payment_hash=data.payment_hash,
|
||||||
labels=data.labels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.lnurl_withdraw:
|
if data.lnurl_withdraw:
|
||||||
@@ -249,7 +241,6 @@ async def create_invoice(
|
|||||||
webhook: str | None = None,
|
webhook: str | None = None,
|
||||||
internal: bool | None = False,
|
internal: bool | None = False,
|
||||||
payment_hash: str | None = None,
|
payment_hash: str | None = None,
|
||||||
labels: list[str] | None = None,
|
|
||||||
conn: Connection | None = None,
|
conn: Connection | None = None,
|
||||||
) -> Payment:
|
) -> Payment:
|
||||||
if not amount > 0:
|
if not amount > 0:
|
||||||
@@ -259,12 +250,6 @@ async def create_invoice(
|
|||||||
if not user_wallet:
|
if not user_wallet:
|
||||||
raise InvoiceError(f"Could not fetch wallet '{wallet_id}'.", status="failed")
|
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]
|
invoice_memo = None if description_hash else memo[:640]
|
||||||
|
|
||||||
# use the fake wallet if the invoice is for internal use only
|
# use the fake wallet if the invoice is for internal use only
|
||||||
@@ -323,7 +308,7 @@ async def create_invoice(
|
|||||||
invoice = bolt11_decode(invoice_response.payment_request)
|
invoice = bolt11_decode(invoice_response.payment_request)
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=user_wallet.source_wallet_id,
|
wallet_id=wallet_id,
|
||||||
bolt11=invoice_response.payment_request,
|
bolt11=invoice_response.payment_request,
|
||||||
payment_hash=invoice.payment_hash,
|
payment_hash=invoice.payment_hash,
|
||||||
preimage=invoice_response.preimage,
|
preimage=invoice_response.preimage,
|
||||||
@@ -333,7 +318,6 @@ async def create_invoice(
|
|||||||
extra=extra,
|
extra=extra,
|
||||||
webhook=webhook,
|
webhook=webhook,
|
||||||
fee=invoice_response.fee_msat or 0,
|
fee=invoice_response.fee_msat or 0,
|
||||||
labels=labels,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
payment = await create_payment(
|
payment = await create_payment(
|
||||||
@@ -472,7 +456,7 @@ async def update_wallet_balance(
|
|||||||
await create_payment(
|
await create_payment(
|
||||||
checking_id=f"internal_{payment_hash}",
|
checking_id=f"internal_{payment_hash}",
|
||||||
data=CreatePayment(
|
data=CreatePayment(
|
||||||
wallet_id=wallet.source_wallet_id,
|
wallet_id=wallet.id,
|
||||||
bolt11=bolt11,
|
bolt11=bolt11,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
amount_msat=amount * 1000,
|
amount_msat=amount * 1000,
|
||||||
@@ -491,7 +475,7 @@ async def update_wallet_balance(
|
|||||||
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
raise ValueError("Balance change failed, amount exceeds maximum balance.")
|
||||||
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
async with db.reuse_conn(conn) if conn else db.connect() as conn:
|
||||||
payment = await create_invoice(
|
payment = await create_invoice(
|
||||||
wallet_id=wallet.source_wallet_id,
|
wallet_id=wallet.id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo="Admin credit",
|
memo="Admin credit",
|
||||||
internal=True,
|
internal=True,
|
||||||
@@ -926,7 +910,7 @@ async def _credit_service_fee_wallet(
|
|||||||
|
|
||||||
memo = f"""
|
memo = f"""
|
||||||
Service fee for payment of {abs(payment.sat)} sats.
|
Service fee for payment of {abs(payment.sat)} sats.
|
||||||
Wallet: '{wallet.name}' ({wallet.source_wallet_id})."""
|
Wallet: '{wallet.name}' ({wallet.id})."""
|
||||||
|
|
||||||
create_payment_model = CreatePayment(
|
create_payment_model = CreatePayment(
|
||||||
wallet_id=settings.lnbits_service_fee_wallet,
|
wallet_id=settings.lnbits_service_fee_wallet,
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
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:
|
while settings.lnbits_running:
|
||||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||||
|
|
||||||
if settings.notification_balance_delta_threshold_sats > 0:
|
if settings.notification_balance_delta_changed:
|
||||||
try:
|
try:
|
||||||
# runs by default every minute, the delta should not change that often
|
# runs by default every minute, the delta should not change that often
|
||||||
await check_balance_delta_changed()
|
await check_balance_delta_changed()
|
||||||
|
|||||||
+6
-17
@@ -1,6 +1,7 @@
|
|||||||
<template id="lnbits-admin-audit">
|
<q-tab-panel name="audit">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">Audit</h6>
|
<h6 class="q-my-none q-mb-sm">Audit</h6>
|
||||||
|
|
||||||
<div class="row q-mb-lg">
|
<div class="row q-mb-lg">
|
||||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
@@ -49,12 +50,8 @@
|
|||||||
<span v-text="$t('audit_record_warning')"></span>
|
<span v-text="$t('audit_record_warning')"></span>
|
||||||
<br />
|
<br />
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li><span v-text="$t('audit_record_req_warning_1')"></span></li>
|
||||||
<span v-text="$t('audit_record_req_warning_1')"></span>
|
<li><span v-text="$t('audit_record_req_warning_2')"></span></li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span v-text="$t('audit_record_req_warning_2')"></span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
<br />
|
||||||
<span v-text="$t('audit_record_use')"></span>
|
<span v-text="$t('audit_record_use')"></span>
|
||||||
@@ -136,15 +133,7 @@
|
|||||||
multiple
|
multiple
|
||||||
:hint="$t('audit_http_methods_hint')"
|
:hint="$t('audit_http_methods_hint')"
|
||||||
:label="$t('audit_http_methods_label')"
|
:label="$t('audit_http_methods_label')"
|
||||||
:options="[
|
:options="['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']"
|
||||||
'GET',
|
|
||||||
'POST',
|
|
||||||
'PUT',
|
|
||||||
'PATCH',
|
|
||||||
'DELETE',
|
|
||||||
'HEAD',
|
|
||||||
'OPTIONS'
|
|
||||||
]"
|
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 q-pr-sm">
|
<div class="col-md-6 col-sm-12 q-pr-sm">
|
||||||
@@ -236,4 +225,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+8
-51
@@ -1,4 +1,4 @@
|
|||||||
<template id="lnbits-admin-exchange-providers">
|
<q-tab-panel name="exchange_providers">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('exchange_providers')"></span>
|
<span v-text="$t('exchange_providers')"></span>
|
||||||
</h6>
|
</h6>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
v-model="props.row.name"
|
v-model="props.row.name"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
v-model="props.row.api_url"
|
v-model="props.row.api_url"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</q-input
|
</q-input
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
dense
|
dense
|
||||||
filled
|
filled
|
||||||
v-model="props.row.path"
|
v-model="props.row.path"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
type="text"
|
type="text"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
@@ -139,9 +139,9 @@
|
|||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="props.row.exclude_to"
|
v-model="props.row.exclude_to"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
multiple
|
multiple
|
||||||
:options="currencies"
|
:options="{{ currencies | safe }}"
|
||||||
></q-select>
|
></q-select>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td>
|
<q-td>
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-chip
|
<q-chip
|
||||||
v-for="(ticker, index) in props.row.ticker_conversion"
|
v-for="ticker, index in props.row.ticker_conversion"
|
||||||
:key="ticker"
|
:key="ticker"
|
||||||
removable
|
removable
|
||||||
dense
|
dense
|
||||||
@@ -198,47 +198,4 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-8 col-sm-12"></div>
|
<div class="col-md-8 col-sm-12"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
<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"
|
|
||||||
></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>
|
|
||||||
</template>
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
<template id="lnbits-admin-extensions">
|
<q-tab-panel name="extensions">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none">
|
||||||
@@ -140,4 +140,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+23
-36
@@ -1,13 +1,14 @@
|
|||||||
<template id="lnbits-admin-fiat-providers">
|
<q-tab-panel name="fiat_providers">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('fiat_providers')"></span>
|
<span v-text="$t('fiat_providers')"></span>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
flat
|
flat
|
||||||
@click="hideInputToggle = !hideInputToggle"
|
@click="hideInputsToggle()"
|
||||||
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-list bordered class="rounded-borders">
|
<q-list bordered class="rounded-borders">
|
||||||
@@ -15,7 +16,9 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar>
|
<q-avatar>
|
||||||
<img src="/static/images/stripe_logo.ico" />
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/stripe_logo.ico') }}'"
|
||||||
|
/>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
@@ -100,9 +103,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<span v-text="$t('webhook_events_list')"></span>
|
<span v-text="$t('webhook_events_list')"></span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li><code>checkout.session.completed</code></li>
|
||||||
<code>checkout.session.completed</code>
|
|
||||||
</li>
|
|
||||||
- the user completed the checkout process
|
- the user completed the checkout process
|
||||||
<li><code>invoice.paid</code></li>
|
<li><code>invoice.paid</code></li>
|
||||||
- the invoice was successfully paid (for subscriptions)
|
- the invoice was successfully paid (for subscriptions)
|
||||||
@@ -119,7 +120,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
v-model="formData.stripe_limits.service_fee_percent"
|
v-model="formData.stripe_limits.service_fee_percent"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('service_fee_label')"
|
:label="$t('service_fee_label')"
|
||||||
:hint="$t('service_fee_hint')"
|
:hint="$t('service_fee_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -131,7 +132,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
v-model="formData.stripe_limits.service_max_fee_sats"
|
v-model="formData.stripe_limits.service_max_fee_sats"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('service_fee_max')"
|
:label="$t('service_fee_max')"
|
||||||
:hint="$t('service_fee_max_hint')"
|
:hint="$t('service_fee_max_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
class="q-ma-sm"
|
class="q-ma-sm"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.stripe_limits.service_fee_wallet_id"
|
v-model="formData.stripe_limits.service_fee_wallet_id"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('fee_wallet_label')"
|
:label="$t('fee_wallet_label')"
|
||||||
:hint="$t('fee_wallet_hint')"
|
:hint="$t('fee_wallet_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
v-model="formData.stripe_limits.service_min_amount_sats"
|
v-model="formData.stripe_limits.service_min_amount_sats"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('min_incoming_payment_amount')"
|
:label="$t('min_incoming_payment_amount')"
|
||||||
:hint="$t('min_incoming_payment_amount_desc')"
|
:hint="$t('min_incoming_payment_amount_desc')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -172,7 +173,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
v-model="formData.stripe_limits.service_max_amount_sats"
|
v-model="formData.stripe_limits.service_max_amount_sats"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('max_incoming_payment_amount')"
|
:label="$t('max_incoming_payment_amount')"
|
||||||
:hint="$t('max_incoming_payment_amount_desc')"
|
:hint="$t('max_incoming_payment_amount_desc')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -182,7 +183,7 @@
|
|||||||
filled
|
filled
|
||||||
class="q-ma-sm"
|
class="q-ma-sm"
|
||||||
v-model="formData.stripe_limits.service_faucet_wallet_id"
|
v-model="formData.stripe_limits.service_faucet_wallet_id"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:label="$t('faucest_wallet_id')"
|
:label="$t('faucest_wallet_id')"
|
||||||
:hint="$t('faucest_wallet_id_hint')"
|
:hint="$t('faucest_wallet_id_hint')"
|
||||||
></q-input>
|
></q-input>
|
||||||
@@ -195,20 +196,12 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
v-text="
|
v-text="$t('faucest_wallet_desc_1', {provider: 'stripe'})"
|
||||||
$t('faucest_wallet_desc_1', {
|
|
||||||
provider: 'stripe'
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
v-text="
|
v-text="$t('faucest_wallet_desc_2', {provider: 'stripe'})"
|
||||||
$t('faucest_wallet_desc_2', {
|
|
||||||
provider: 'stripe'
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -216,11 +209,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span
|
<span
|
||||||
v-text="
|
v-text="$t('faucest_wallet_desc_4', {provider: 'stripe'})"
|
||||||
$t('faucest_wallet_desc_4', {
|
|
||||||
provider: 'stripe'
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></span>
|
></span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -238,14 +227,10 @@
|
|||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
v-model="formAddStripeUser"
|
v-model="formAddStripeUser"
|
||||||
@keydown.enter="addStripeAllowedUser"
|
@keydown.enter="addAllowedUser"
|
||||||
type="text"
|
type="text"
|
||||||
:label="$t('allowed_users_label')"
|
:label="$t('allowed_users_label')"
|
||||||
:hint="
|
:hint="$t('allowed_users_hint_feature', {feature: 'Stripe'})"
|
||||||
$t('allowed_users_hint_feature', {
|
|
||||||
feature: 'Stripe'
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="addStripeAllowedUser"
|
@click="addStripeAllowedUser"
|
||||||
@@ -257,7 +242,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<q-chip
|
<q-chip
|
||||||
v-for="user in formData.stripe_limits.allowed_users"
|
v-for="user in formData.stripe_limits.allowed_users"
|
||||||
@update:model-value="formData.touch = null"
|
@update:model-value="touchSettings()"
|
||||||
:key="user"
|
:key="user"
|
||||||
removable
|
removable
|
||||||
@remove="removeStripeAllowedUser(user)"
|
@remove="removeStripeAllowedUser(user)"
|
||||||
@@ -279,7 +264,9 @@
|
|||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar>
|
<q-avatar>
|
||||||
<img src="/static/images/square_logo.png" />
|
<img
|
||||||
|
:src="'{{ static_url_for('static', 'images/square_logo.png') }}'"
|
||||||
|
/>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
@@ -297,4 +284,4 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+33
-65
@@ -1,4 +1,4 @@
|
|||||||
<template id="lnbits-admin-funding">
|
<q-tab-panel name="funding">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none">
|
||||||
<span v-text="$t('wallets_management')"></span>
|
<span v-text="$t('wallets_management')"></span>
|
||||||
@@ -12,67 +12,45 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-text="
|
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
|
||||||
$t('funding_source', {
|
|
||||||
wallet_class: settings.lnbits_backend_wallet_class
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="
|
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
|
||||||
$t('node_balance', {
|
|
||||||
balance: (auditData.node_balance_sats || 0).toLocaleString()
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="
|
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
|
||||||
$t('lnbits_balance', {
|
|
||||||
balance: (auditData.lnbits_balance_sats || 0).toLocaleString()
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></li>
|
></li>
|
||||||
<li
|
<li
|
||||||
v-text="
|
v-text="$t('funding_reserve_percent', {
|
||||||
$t('funding_reserve_percent', {
|
percent: auditData.lnbits_balance_sats > 0
|
||||||
percent:
|
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
|
||||||
auditData.lnbits_balance_sats > 0
|
: 100
|
||||||
? (
|
})"
|
||||||
(auditData.node_balance_sats /
|
|
||||||
auditData.lnbits_balance_sats) *
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
: 100
|
|
||||||
})
|
|
||||||
"
|
|
||||||
></li>
|
></li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div v-if="LNBITS_NODE_UI">
|
{% if LNBITS_NODE_UI_AVAILABLE %}
|
||||||
<p>
|
<p><span v-text="$t('node_management')"></span></p>
|
||||||
<span v-text="$t('node_management')"></span>
|
<q-toggle
|
||||||
</p>
|
:label="$t('toggle_node_ui')"
|
||||||
<q-toggle
|
v-model="formData.lnbits_node_ui"
|
||||||
:label="$t('toggle_node_ui')"
|
></q-toggle>
|
||||||
v-model="formData.lnbits_node_ui"
|
<q-toggle
|
||||||
></q-toggle>
|
v-if="formData.lnbits_node_ui"
|
||||||
<q-toggle
|
:label="$t('toggle_public_node_ui')"
|
||||||
v-if="formData.lnbits_node_ui"
|
v-model="formData.lnbits_public_node_ui"
|
||||||
:label="$t('toggle_public_node_ui')"
|
></q-toggle>
|
||||||
v-model="formData.lnbits_public_node_ui"
|
<br />
|
||||||
></q-toggle>
|
<q-toggle
|
||||||
<br />
|
v-if="formData.lnbits_node_ui"
|
||||||
<q-toggle
|
:label="$t('toggle_transactions_node_ui')"
|
||||||
v-if="formData.lnbits_node_ui"
|
v-model="formData.lnbits_node_ui_transactions"
|
||||||
:label="$t('toggle_transactions_node_ui')"
|
></q-toggle>
|
||||||
v-model="formData.lnbits_node_ui_transactions"
|
{% else %}
|
||||||
></q-toggle>
|
<p><span v-text="$t('node_management_not_supported')"></span></p>
|
||||||
</div>
|
{% endif %}
|
||||||
<p v-if="!LNBITS_NODE_UI">
|
|
||||||
<span v-text="$t('node_management_not_supported')"></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
@@ -88,9 +66,7 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<p>
|
<p><span v-text="$t('fee_reserve_percent')"></span></p>
|
||||||
<span v-text="$t('fee_reserve_percent')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
type="number"
|
type="number"
|
||||||
filled
|
filled
|
||||||
@@ -112,15 +88,7 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<p>
|
<p><span v-text="$t('payment_wait_time')"></span></p>
|
||||||
<span v-text="$t('payment_wait_time')"></span>
|
|
||||||
<sup>
|
|
||||||
<q-icon name="info" size="16px" class="q-ml-xs"></q-icon>
|
|
||||||
<q-tooltip max-width="150px">
|
|
||||||
<span v-text="$t('payment_wait_time_tooltip')"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</sup>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
type="number"
|
type="number"
|
||||||
filled
|
filled
|
||||||
@@ -134,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isSuperUser">
|
<div v-if="isSuperUser">
|
||||||
<lnbits-admin-funding-sources
|
<lnbits-funding-sources
|
||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||||
/>
|
/>
|
||||||
@@ -247,4 +215,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<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>
|
||||||
+10
-15
@@ -1,4 +1,4 @@
|
|||||||
<template id="lnbits-admin-notifications">
|
<q-tab-panel name="notifications">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('notifications_configure')"></span>
|
<span v-text="$t('notifications_configure')"></span>
|
||||||
@@ -404,14 +404,13 @@
|
|||||||
v-text="$t('notification_balance_delta_changed_desc')"
|
v-text="$t('notification_balance_delta_changed_desc')"
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-input
|
<q-toggle
|
||||||
class="flow-right"
|
size="md"
|
||||||
type="number"
|
v-model="formData.notification_balance_delta_changed"
|
||||||
min="0"
|
checked-icon="check"
|
||||||
filled
|
color="green"
|
||||||
v-model="formData.notification_balance_delta_threshold_sats"
|
unchecked-icon="clear"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -479,9 +478,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
filled
|
filled
|
||||||
v-model="
|
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
|
||||||
formData.lnbits_notification_incoming_payment_amount_sats
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -504,13 +501,11 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
filled
|
filled
|
||||||
v-model="
|
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
|
||||||
formData.lnbits_notification_outgoing_payment_amount_sats
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+6
-15
@@ -1,8 +1,6 @@
|
|||||||
<template id="lnbits-admin-security">
|
<q-tab-panel name="security">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('server_management')"></span></h6>
|
||||||
<span v-text="$t('server_management')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('base_url')"></span></p>
|
<p><span v-text="$t('base_url')"></span></p>
|
||||||
@@ -36,10 +34,7 @@
|
|||||||
:hint="$t('auth_allowed_methods_hint')"
|
:hint="$t('auth_allowed_methods_hint')"
|
||||||
:label="$t('auth_allowed_methods_label')"
|
:label="$t('auth_allowed_methods_label')"
|
||||||
:options="formData.auth_all_methods"
|
:options="formData.auth_all_methods"
|
||||||
:option-label="
|
:option-label="option => option.length > 25 ? option.substring(0, 22) + '...' : option"
|
||||||
option =>
|
|
||||||
option.length > 25 ? option.substring(0, 22) + '...' : option
|
|
||||||
"
|
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,11 +206,7 @@
|
|||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="
|
:label="(serverlogEnabled) ? $t('disable_server_log') : $t('enable_server_log')"
|
||||||
serverlogEnabled
|
|
||||||
? $t('disable_server_log')
|
|
||||||
: $t('enable_server_log')
|
|
||||||
"
|
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
@@ -300,7 +291,7 @@
|
|||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
:options="[$t('second'), $t('minute'), $t('hour')]"
|
:options="[$t('second'),$t('minute'),$t('hour')]"
|
||||||
v-model="formData.lnbits_rate_limit_unit"
|
v-model="formData.lnbits_rate_limit_unit"
|
||||||
:label="$t('time_unit')"
|
:label="$t('time_unit')"
|
||||||
></q-select>
|
></q-select>
|
||||||
@@ -346,4 +337,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+13
-35
@@ -1,51 +1,37 @@
|
|||||||
<template id="lnbits-admin-server">
|
<q-tab-panel name="server">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<div>
|
<div>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('currency_settings')"></span></h6>
|
||||||
<span v-text="$t('currency_settings')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p><span v-text="$t('allowed_currencies')"></span></p>
|
||||||
<span v-text="$t('allowed_currencies')"></span>
|
|
||||||
</p>
|
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_allowed_currencies"
|
v-model="formData.lnbits_allowed_currencies"
|
||||||
multiple
|
multiple
|
||||||
:hint="$t('allowed_currencies_hint')"
|
:hint="$t('allowed_currencies_hint')"
|
||||||
:label="$t('allowed_currencies')"
|
:label="$t('allowed_currencies')"
|
||||||
:options="g.currencies"
|
:options="{{ currencies | safe }}"
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p><span v-text="$t('default_account_currency')"></span></p>
|
||||||
<span v-text="$t('default_account_currency')"></span>
|
|
||||||
</p>
|
|
||||||
<q-select
|
<q-select
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_default_accounting_currency"
|
v-model="formData.lnbits_default_accounting_currency"
|
||||||
clearable
|
clearable
|
||||||
:hint="$t('default_account_currency_hint')"
|
:hint="$t('default_account_currency_hint')"
|
||||||
:label="$t('currency')"
|
:label="$t('currency')"
|
||||||
:options="
|
:options="formData.lnbits_allowed_currencies?.length ? formData.lnbits_allowed_currencies : {{ currencies }}"
|
||||||
formData.lnbits_allowed_currencies?.length
|
|
||||||
? formData.lnbits_allowed_currencies
|
|
||||||
: g.allowedCurrencies
|
|
||||||
"
|
|
||||||
></q-select>
|
></q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('payments')"></span></h6>
|
||||||
<span v-text="$t('payments')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<p>
|
<p><span v-text="$t('max_outgoing_payment_amount')"></span></p>
|
||||||
<span v-text="$t('max_outgoing_payment_amount')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="number"
|
type="number"
|
||||||
@@ -58,9 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<p>
|
<p><span v-text="$t('max_incoming_payment_amount')"></span></p>
|
||||||
<span v-text="$t('max_incoming_payment_amount')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="number"
|
type="number"
|
||||||
@@ -75,9 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('wallet_limiter')"></span></h6>
|
||||||
<span v-text="$t('wallet_limiter')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-3">
|
<div class="col-12 col-md-3">
|
||||||
<q-input
|
<q-input
|
||||||
@@ -113,9 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
<q-separator class="q-mb-lg q-mt-sm"></q-separator>
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('service_fee')"></span></h6>
|
||||||
<span v-text="$t('service_fee')"></span>
|
|
||||||
</h6>
|
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('service_fee')"></span></p>
|
<p><span v-text="$t('service_fee')"></span></p>
|
||||||
@@ -151,9 +131,7 @@
|
|||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p>
|
<p><span v-text="$t('disable_fee_internal')"></span></p>
|
||||||
<span v-text="$t('disable_fee_internal')"></span>
|
|
||||||
</p>
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label v-text="$t('disable_fee')"></q-item-label>
|
<q-item-label v-text="$t('disable_fee')"></q-item-label>
|
||||||
@@ -177,4 +155,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+8
-24
@@ -1,8 +1,6 @@
|
|||||||
<template id="lnbits-admin-site-customisation">
|
<q-tab-panel name="site_customisation">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none">
|
<h6 class="q-my-none"><span v-text="$t('ui_management')"></span></h6>
|
||||||
<span v-text="$t('ui_management')"></span>
|
|
||||||
</h6>
|
|
||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
@@ -12,9 +10,7 @@
|
|||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.lnbits_site_title"
|
v-model="formData.lnbits_site_title"
|
||||||
:label="
|
:label="$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')"
|
||||||
$t('ui_site_title') + $t('ui_changing_remove_lnbits_elements')
|
|
||||||
"
|
|
||||||
></q-input>
|
></q-input>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -32,19 +28,13 @@
|
|||||||
<q-toggle
|
<q-toggle
|
||||||
:tip="$t('ui_toggle_elements_tip')"
|
:tip="$t('ui_toggle_elements_tip')"
|
||||||
v-model="formData.lnbits_show_home_page_elements"
|
v-model="formData.lnbits_show_home_page_elements"
|
||||||
:label="
|
:label="formData.lnbits_show_home_page_elements ? $t('ui_elements_enable') : $t('ui_elements_disable')"
|
||||||
formData.lnbits_show_home_page_elements
|
|
||||||
? $t('ui_elements_enable')
|
|
||||||
: $t('ui_elements_disable')
|
|
||||||
"
|
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p><span v-text="$t('ui_site_description')"></span></p>
|
||||||
<span v-text="$t('ui_site_description')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="formData.lnbits_site_description"
|
v-model="formData.lnbits_site_description"
|
||||||
filled
|
filled
|
||||||
@@ -55,9 +45,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p>
|
<p><span v-text="$t('ui_default_wallet_name')"></span></p>
|
||||||
<span v-text="$t('ui_default_wallet_name')"></span>
|
|
||||||
</p>
|
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
type="text"
|
type="text"
|
||||||
@@ -163,11 +151,7 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="formData.lnbits_ad_space_enabled"
|
v-model="formData.lnbits_ad_space_enabled"
|
||||||
:label="
|
:label="formData.lnbits_ad_space_enabled ? $t('ads_enabled') : $t('ads_disabled')"
|
||||||
formData.lnbits_ad_space_enabled
|
|
||||||
? $t('ads_enabled')
|
|
||||||
: $t('ads_disabled')
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -223,4 +207,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
<template id="lnbits-admin-users">
|
<q-tab-panel name="users">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('user_management')"></span>
|
<span v-text="$t('user_management')"></span>
|
||||||
@@ -80,4 +80,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</q-tab-panel>
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
{% 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>
|
||||||
|
<!-- Mobile: Horizontal tabs at top -->
|
||||||
|
<q-tabs
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
@update:model-value="showExchangeProvidersTab"
|
||||||
|
v-model="tab"
|
||||||
|
dense
|
||||||
|
active-color="primary"
|
||||||
|
inline-label
|
||||||
|
class="text-primary"
|
||||||
|
>
|
||||||
|
<q-tab name="funding" icon="account_balance_wallet" label="Fund" />
|
||||||
|
<q-tab name="security" icon="security" label="Sec" />
|
||||||
|
<q-tab name="server" icon="settings" label="Srv" />
|
||||||
|
<q-tab name="exchange_providers" icon="swap_horiz" label="Exch" />
|
||||||
|
<q-tab name="fiat_providers" icon="account_balance" label="Fiat" />
|
||||||
|
<q-tab name="extensions" icon="extension" label="Ext" />
|
||||||
|
<q-tab name="notifications" icon="notifications" label="Not" />
|
||||||
|
<q-tab name="audit" icon="receipt_long" label="Aud" />
|
||||||
|
<q-tab name="site_customisation" icon="language" label="Site" />
|
||||||
|
<q-tab name="library" icon="image" label="Lib" />
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<!-- Desktop: Vertical sidebar with splitter -->
|
||||||
|
<q-splitter v-if="$q.screen.gt.sm">
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- Mobile: Full-width content without sidebar -->
|
||||||
|
<q-form
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
name="settings_form"
|
||||||
|
id="settings_form_mobile"
|
||||||
|
>
|
||||||
|
<q-scroll-area style="height: 70vh">
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="tab"
|
||||||
|
animated
|
||||||
|
vertical
|
||||||
|
scroll
|
||||||
|
transition-prev="jump-up"
|
||||||
|
transition-next="jump-up"
|
||||||
|
style="overflow-x: hidden; padding: 8px"
|
||||||
|
>
|
||||||
|
{% 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>
|
||||||
|
</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 %}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
<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
@@ -0,0 +1,830 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
{% 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
@@ -0,0 +1,165 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
|
||||||
%} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}{%
|
|
||||||
endblock %}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
|
||||||
context %} {% block scripts %} {{ window_vars() }} {% endblock %} {% block page
|
|
||||||
%} {% endblock %}
|
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
{% 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 %}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
{% 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,14 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from shutil import make_archive
|
from pathlib import Path
|
||||||
|
from shutil import make_archive, move
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File
|
import filetype
|
||||||
|
from fastapi import APIRouter, Depends, File, Header, HTTPException, UploadFile
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
|
from lnbits.core.models.misc import Image, SimpleStatus
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
from lnbits.core.services import (
|
||||||
enqueue_admin_notification,
|
enqueue_admin_notification,
|
||||||
@@ -18,6 +23,7 @@ from lnbits.core.services import (
|
|||||||
from lnbits.core.services.notifications import send_email_notification
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
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.server import server_restart
|
||||||
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
from lnbits.settings import AdminSettings, Settings, UpdateSettings, settings
|
||||||
from lnbits.tasks import invoice_listeners
|
from lnbits.tasks import invoice_listeners
|
||||||
@@ -166,3 +172,93 @@ async def api_download_backup() -> FileResponse:
|
|||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=f"{last_filename}.zip", filename=filename, media_type="application/zip"
|
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")
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
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,7 +25,6 @@ from lnbits.core.models.users import (
|
|||||||
UpdateAccessControlList,
|
UpdateAccessControlList,
|
||||||
)
|
)
|
||||||
from lnbits.core.services import create_user_account
|
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.decorators import access_token_payload, check_user_exists
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
@@ -423,6 +422,15 @@ async def update(
|
|||||||
) -> User | None:
|
) -> User | None:
|
||||||
if data.user_id != user.id:
|
if data.user_id != user.id:
|
||||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid 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)
|
account = await get_account(user.id)
|
||||||
if not account:
|
if not account:
|
||||||
@@ -433,7 +441,7 @@ async def update(
|
|||||||
if data.extra:
|
if data.extra:
|
||||||
account.extra = data.extra
|
account.extra = data.extra
|
||||||
|
|
||||||
await update_user_account(account)
|
await update_account(account)
|
||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,12 @@ from lnbits.decorators import (
|
|||||||
check_admin,
|
check_admin,
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
)
|
)
|
||||||
from lnbits.settings import settings
|
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_user_extension,
|
create_user_extension,
|
||||||
delete_dbversion,
|
delete_dbversion,
|
||||||
drop_extension_db,
|
drop_extension_db,
|
||||||
get_db_version,
|
get_db_version,
|
||||||
get_db_versions,
|
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
get_user_extension,
|
get_user_extension,
|
||||||
@@ -494,82 +492,3 @@ async def delete_extension_db(ext_id: str):
|
|||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Cannot delete data for extension '{ext_id}'",
|
detail=f"Cannot delete data for extension '{ext_id}'",
|
||||||
) from exc
|
) 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,8 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
@@ -27,9 +28,9 @@ from lnbits.core.services.extensions_builder import (
|
|||||||
)
|
)
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_admin,
|
check_admin,
|
||||||
check_extension_builder,
|
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
)
|
)
|
||||||
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_user_extension,
|
create_user_extension,
|
||||||
@@ -46,12 +47,19 @@ extension_builder_router = APIRouter(
|
|||||||
@extension_builder_router.post(
|
@extension_builder_router.post(
|
||||||
"/zip",
|
"/zip",
|
||||||
summary="Build and download extension zip.",
|
summary="Build and download extension zip.",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
|
||||||
description="""
|
description="""
|
||||||
This endpoint generates a zip file for the extension based on the provided data.
|
This endpoint generates a zip file for the extension based on the provided data.
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
async def api_build_extension(data: ExtensionData) -> FileResponse:
|
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.",
|
||||||
|
)
|
||||||
stub_ext_id = "extension_builder_stub" # todo: do not hardcode, fetch from manifest
|
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)
|
release, build_dir = await build_extension_from_data(data, stub_ext_id)
|
||||||
|
|
||||||
@@ -124,12 +132,16 @@ async def api_deploy_extension(
|
|||||||
@extension_builder_router.post(
|
@extension_builder_router.post(
|
||||||
"/preview",
|
"/preview",
|
||||||
summary="Build and preview the extension ui.",
|
summary="Build and preview the extension ui.",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
|
||||||
)
|
)
|
||||||
async def api_preview_extension(
|
async def api_preview_extension(
|
||||||
data: ExtensionData,
|
data: ExtensionData,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> SimpleStatus:
|
) -> 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"
|
stub_ext_id = "extension_builder_stub"
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||||
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
await build_extension_from_data(data, stub_ext_id, working_dir_name)
|
||||||
|
|||||||
+335
-35
@@ -1,29 +1,35 @@
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Annotated
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import Depends, Request
|
from fastapi import Cookie, Depends, Query, Request
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from lnurl import url_decode
|
from lnurl import url_decode
|
||||||
|
from pydantic.types import UUID4
|
||||||
|
|
||||||
from lnbits.core.helpers import to_valid_user_id
|
from lnbits.core.helpers import to_valid_user_id
|
||||||
from lnbits.core.models import User
|
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 import create_invoice, create_user_account
|
||||||
from lnbits.decorators import (
|
from lnbits.core.services.extensions import get_valid_extensions
|
||||||
check_admin,
|
from lnbits.decorators import check_admin, check_user_exists
|
||||||
check_admin_ui,
|
|
||||||
check_extension_builder,
|
|
||||||
check_first_install,
|
|
||||||
check_user_exists,
|
|
||||||
)
|
|
||||||
from lnbits.helpers import check_callback_url, template_renderer
|
from lnbits.helpers import check_callback_url, template_renderer
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
from lnbits.wallets import get_funding_source
|
||||||
|
|
||||||
from ..crud import get_user
|
from ...utils.exchange_rates import allowed_currencies, currencies
|
||||||
|
from ..crud import (
|
||||||
|
create_wallet,
|
||||||
|
get_db_versions,
|
||||||
|
get_installed_extensions,
|
||||||
|
get_user,
|
||||||
|
get_wallet,
|
||||||
|
)
|
||||||
|
|
||||||
generic_router = APIRouter(
|
generic_router = APIRouter(
|
||||||
tags=["Core NON-API Website Routes"], include_in_schema=False
|
tags=["Core NON-API Website Routes"], include_in_schema=False
|
||||||
@@ -35,23 +41,251 @@ async def favicon():
|
|||||||
return RedirectResponse(settings.lnbits_qr_logo)
|
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)
|
@generic_router.get("/robots.txt", response_class=HTMLResponse)
|
||||||
async def robots():
|
async def robots():
|
||||||
data = "User-agent: *\nDisallow: /"
|
data = """
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
"""
|
||||||
return HTMLResponse(content=data, media_type="text/plain")
|
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(
|
@generic_router.get(
|
||||||
"/extensions/builder/preview/{ext_id}",
|
"/extensions/builder/preview/{ext_id}",
|
||||||
name="extensions builder",
|
name="extensions builder",
|
||||||
dependencies=[Depends(check_extension_builder)],
|
response_class=HTMLResponse,
|
||||||
)
|
)
|
||||||
async def extensions_builder_preview(
|
async def extensions_builder_preview(
|
||||||
request: Request,
|
request: Request,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
page_name: str | None = None,
|
page_name: str | None = None,
|
||||||
user: User = Depends(check_user_exists),
|
user: User = Depends(check_user_exists),
|
||||||
) -> HTMLResponse:
|
):
|
||||||
|
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.",
|
||||||
|
)
|
||||||
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
working_dir_name = "preview_" + sha256(user.id.encode("utf-8")).hexdigest()
|
||||||
html_file_name = "index.html"
|
html_file_name = "index.html"
|
||||||
if page_name == "public_page":
|
if page_name == "public_page":
|
||||||
@@ -125,7 +359,7 @@ async def manifest(request: Request, usr: str):
|
|||||||
"src": (
|
"src": (
|
||||||
settings.lnbits_custom_logo
|
settings.lnbits_custom_logo
|
||||||
if settings.lnbits_custom_logo
|
if settings.lnbits_custom_logo
|
||||||
else "images/logos/lnbits.png"
|
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
|
||||||
),
|
),
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
@@ -192,38 +426,104 @@ async def manifest(request: Request, usr: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
admin_ui_checks = [Depends(check_admin), Depends(check_admin_ui)]
|
@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)
|
||||||
|
|
||||||
|
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(
|
return template_renderer().TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"index.html",
|
"node/index.html",
|
||||||
{
|
{
|
||||||
"user": user.json(),
|
"user": user.json(),
|
||||||
|
"balance": balance,
|
||||||
|
"wallets": user.wallets[0].json(),
|
||||||
|
"ajax": _is_ajax_request(request),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/")
|
@generic_router.get("/node/public", response_class=HTMLResponse)
|
||||||
@generic_router.get("/node/public")
|
async def node_public(request: Request):
|
||||||
@generic_router.get("/first_install", dependencies=[Depends(check_first_install)])
|
if not settings.lnbits_public_node_ui:
|
||||||
async def index_public(request: Request) -> HTMLResponse:
|
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
|
||||||
return template_renderer().TemplateResponse(request, "index_public.html")
|
|
||||||
|
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("/uuidv4/{hex_value}")
|
@generic_router.get("/uuidv4/{hex_value}")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from http import HTTPStatus
|
|||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
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.decorators import check_admin, check_super_user, parse_filters
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -153,7 +154,7 @@ async def api_get_payments(
|
|||||||
) -> Page[NodePayment] | None:
|
) -> Page[NodePayment] | None:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail="You can enable node transactions in the Admin UI",
|
detail="You can enable node transactions in the Admin UI",
|
||||||
)
|
)
|
||||||
return await node.get_payments(filters)
|
return await node.get_payments(filters)
|
||||||
@@ -166,7 +167,7 @@ async def api_get_invoices(
|
|||||||
) -> Page[NodeInvoice] | None:
|
) -> Page[NodeInvoice] | None:
|
||||||
if not settings.lnbits_node_ui_transactions:
|
if not settings.lnbits_node_ui_transactions:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
detail="You can enable node transactions in the Admin UI",
|
detail="You can enable node transactions in the Admin UI",
|
||||||
)
|
)
|
||||||
return await node.get_invoices(filters)
|
return await node.get_invoices(filters)
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ from lnbits import bolt11
|
|||||||
from lnbits.core.crud.payments import (
|
from lnbits.core.crud.payments import (
|
||||||
get_payment_count_stats,
|
get_payment_count_stats,
|
||||||
get_wallets_stats,
|
get_wallets_stats,
|
||||||
update_payment,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.users import get_account
|
|
||||||
from lnbits.core.models import (
|
from lnbits.core.models import (
|
||||||
CancelInvoice,
|
CancelInvoice,
|
||||||
CreateInvoice,
|
CreateInvoice,
|
||||||
@@ -34,7 +32,6 @@ from lnbits.core.models import (
|
|||||||
SettleInvoice,
|
SettleInvoice,
|
||||||
SimpleStatus,
|
SimpleStatus,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.payments import UpdatePaymentLabels
|
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
@@ -179,18 +176,12 @@ async def api_payments_daily_stats(
|
|||||||
)
|
)
|
||||||
async def api_payments_paginated(
|
async def api_payments_paginated(
|
||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
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)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
if not recheck_pending:
|
|
||||||
return page
|
|
||||||
|
|
||||||
for payment in page.data:
|
for payment in page.data:
|
||||||
if payment.pending:
|
if payment.pending:
|
||||||
await update_pending_payment(payment)
|
await update_pending_payment(payment)
|
||||||
@@ -256,7 +247,6 @@ async def api_payments_create(
|
|||||||
wallet_id=wallet_id,
|
wallet_id=wallet_id,
|
||||||
payment_request=invoice_data.bolt11,
|
payment_request=invoice_data.bolt11,
|
||||||
extra=invoice_data.extra,
|
extra=invoice_data.extra,
|
||||||
labels=invoice_data.labels,
|
|
||||||
)
|
)
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
@@ -270,27 +260,6 @@ async def api_payments_create(
|
|||||||
return await create_payment_request(wallet_id, invoice_data)
|
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")
|
@payment_router.get("/fee-reserve")
|
||||||
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
async def api_payments_fee_reserve(invoice: str = Query("invoice")) -> JSONResponse:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
|||||||
@@ -8,25 +8,10 @@ from fastapi import (
|
|||||||
HTTPException,
|
HTTPException,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lnbits.core.crud.wallets import (
|
from lnbits.core.crud.wallets import get_wallets_paginated
|
||||||
create_wallet,
|
|
||||||
get_wallets_paginated,
|
|
||||||
)
|
|
||||||
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
from lnbits.core.models import CreateWallet, KeyType, User, Wallet, WalletTypeInfo
|
||||||
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
from lnbits.core.models.lnurl import StoredPayLink, StoredPayLinks
|
||||||
from lnbits.core.models.misc import SimpleStatus
|
from lnbits.core.models.wallets import WalletsFilters
|
||||||
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.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_user_exists,
|
check_user_exists,
|
||||||
@@ -37,6 +22,7 @@ from lnbits.decorators import (
|
|||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import generate_filter_params_openapi
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
|
create_wallet,
|
||||||
delete_wallet,
|
delete_wallet,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
update_wallet,
|
update_wallet,
|
||||||
@@ -76,35 +62,6 @@ async def api_wallets_paginated(
|
|||||||
return page
|
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}")
|
@wallet_router.put("/{new_name}")
|
||||||
async def api_update_wallet_name(
|
async def api_update_wallet_name(
|
||||||
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
new_name: str, key_info: WalletTypeInfo = Depends(require_admin_key)
|
||||||
@@ -113,7 +70,6 @@ async def api_update_wallet_name(
|
|||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Wallet not found")
|
||||||
wallet.name = new_name
|
wallet.name = new_name
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return {
|
return {
|
||||||
"id": wallet.id,
|
"id": wallet.id,
|
||||||
@@ -168,7 +124,6 @@ async def api_update_wallet(
|
|||||||
wallet.extra.color = color or wallet.extra.color
|
wallet.extra.color = color or wallet.extra.color
|
||||||
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
wallet.extra.pinned = pinned if pinned is not None else wallet.extra.pinned
|
||||||
wallet.currency = currency if currency is not None else wallet.currency
|
wallet.currency = currency if currency is not None else wallet.currency
|
||||||
|
|
||||||
await update_wallet(wallet)
|
await update_wallet(wallet)
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
@@ -192,21 +147,4 @@ async def api_create_wallet(
|
|||||||
data: CreateWallet,
|
data: CreateWallet,
|
||||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> Wallet:
|
) -> Wallet:
|
||||||
if data.wallet_type == WalletType.LIGHTNING:
|
return await create_wallet(user_id=key_info.wallet.user, wallet_name=data.name)
|
||||||
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}.",
|
|
||||||
)
|
|
||||||
|
|||||||
+25
-92
@@ -130,12 +130,6 @@ class Compat:
|
|||||||
return "BIGINT"
|
return "BIGINT"
|
||||||
return "INT"
|
return "INT"
|
||||||
|
|
||||||
@property
|
|
||||||
def blob(self) -> str:
|
|
||||||
if self.type in {POSTGRES}:
|
|
||||||
return "BYTEA"
|
|
||||||
return "BLOB"
|
|
||||||
|
|
||||||
def timestamp_placeholder(self, key: str) -> str:
|
def timestamp_placeholder(self, key: str) -> str:
|
||||||
return compat_timestamp_placeholder(key)
|
return compat_timestamp_placeholder(key)
|
||||||
|
|
||||||
@@ -222,34 +216,18 @@ class Connection(Compat):
|
|||||||
filters: Filters | None = None,
|
filters: Filters | None = None,
|
||||||
model: type[TModel] | None = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: list[str] | None = None,
|
||||||
table_name: str | None = None,
|
|
||||||
) -> Page[TModel]:
|
) -> 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:
|
if not filters:
|
||||||
filters = 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)
|
clause = filters.where(where)
|
||||||
parsed_values = filters.values(values)
|
parsed_values = filters.values(values)
|
||||||
|
|
||||||
group_by_string = ""
|
group_by_string = ""
|
||||||
if group_by:
|
if group_by:
|
||||||
for field in group_by:
|
for field in group_by:
|
||||||
if not _valid_sql_name(field):
|
if not re.fullmatch(
|
||||||
|
r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?", field
|
||||||
|
):
|
||||||
raise ValueError("Value for GROUP BY is invalid")
|
raise ValueError("Value for GROUP BY is invalid")
|
||||||
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
group_by_string = f"GROUP BY {', '.join(group_by)}"
|
||||||
|
|
||||||
@@ -267,17 +245,16 @@ class Connection(Compat):
|
|||||||
if rows:
|
if rows:
|
||||||
# no need for extra query if no pagination is specified
|
# no need for extra query if no pagination is specified
|
||||||
if filters.offset or filters.limit:
|
if filters.offset or filters.limit:
|
||||||
if table_name:
|
result = await self.execute(
|
||||||
count_query = f"SELECT COUNT(*) as count FROM {table_name} {clause}" # noqa: S608
|
f"""
|
||||||
else:
|
SELECT COUNT(*) as count FROM (
|
||||||
count_query = f"""SELECT COUNT(*) as count
|
|
||||||
FROM (
|
|
||||||
{query}
|
{query}
|
||||||
{clause}
|
{clause}
|
||||||
{group_by_string}
|
{group_by_string}
|
||||||
) as count""" # noqa: S608
|
) as count
|
||||||
|
""", # noqa: S608
|
||||||
result = await self.execute(count_query, parsed_values)
|
parsed_values,
|
||||||
|
)
|
||||||
row = result.mappings().first()
|
row = result.mappings().first()
|
||||||
result.close()
|
result.close()
|
||||||
count = int(row.get("count", 0))
|
count = int(row.get("count", 0))
|
||||||
@@ -410,12 +387,9 @@ class Database(Compat):
|
|||||||
filters: Filters | None = None,
|
filters: Filters | None = None,
|
||||||
model: type[TModel] | None = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: list[str] | None = None,
|
group_by: list[str] | None = None,
|
||||||
table_name: str | None = None,
|
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetch_page(
|
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||||
query, where, values, filters, model, group_by, table_name
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute(self, query: str, values: dict | None = None):
|
async def execute(self, query: str, values: dict | None = None):
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
@@ -451,8 +425,6 @@ class Operator(Enum):
|
|||||||
INCLUDE = "in"
|
INCLUDE = "in"
|
||||||
EXCLUDE = "ex"
|
EXCLUDE = "ex"
|
||||||
LIKE = "like"
|
LIKE = "like"
|
||||||
EVERY = "every"
|
|
||||||
ANY = "any"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_sql(self):
|
def as_sql(self):
|
||||||
@@ -472,7 +444,7 @@ class Operator(Enum):
|
|||||||
return ">="
|
return ">="
|
||||||
elif self == Operator.LE:
|
elif self == Operator.LE:
|
||||||
return "<="
|
return "<="
|
||||||
elif self in {Operator.LIKE, Operator.EVERY, Operator.ANY}:
|
elif self == Operator.LIKE:
|
||||||
return "LIKE"
|
return "LIKE"
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown SQL Operator")
|
raise ValueError("Unknown SQL Operator")
|
||||||
@@ -494,7 +466,6 @@ class Page(BaseModel, Generic[T]):
|
|||||||
|
|
||||||
|
|
||||||
class Filter(BaseModel, Generic[TFilterModel]):
|
class Filter(BaseModel, Generic[TFilterModel]):
|
||||||
table_name: str | None = None
|
|
||||||
field: str
|
field: str
|
||||||
op: Operator = Operator.EQ
|
op: Operator = Operator.EQ
|
||||||
model: type[TFilterModel] | None
|
model: type[TFilterModel] | None
|
||||||
@@ -504,8 +475,6 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
def parse_query(
|
def parse_query(
|
||||||
cls, key: str, raw_values: list[Any], model: type[TFilterModel], i: int = 0
|
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 format:
|
||||||
# key[operator]
|
# key[operator]
|
||||||
# e.g. name[eq]
|
# e.g. name[eq]
|
||||||
@@ -522,14 +491,11 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
if field in model.__fields__:
|
if field in model.__fields__:
|
||||||
compare_field = model.__fields__[field]
|
compare_field = model.__fields__[field]
|
||||||
values: dict = {}
|
values: dict = {}
|
||||||
if op in {Operator.EVERY, Operator.ANY, Operator.INCLUDE, Operator.EXCLUDE}:
|
for raw_value in raw_values:
|
||||||
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")
|
validated, errors = compare_field.validate(raw_value, {}, loc="none")
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors=[errors], model=model)
|
raise ValidationError(errors=[errors], model=model)
|
||||||
values[f"{field}__{index}"] = validated
|
values[f"{field}__{i}"] = validated
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown filter field")
|
raise ValueError("Unknown filter field")
|
||||||
|
|
||||||
@@ -537,24 +503,15 @@ class Filter(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def statement(self) -> str:
|
def statement(self) -> str:
|
||||||
prefix = f"{self.table_name}." if self.table_name else ""
|
|
||||||
stmt = []
|
stmt = []
|
||||||
for key in self.values.keys() if self.values else []:
|
for key in self.values.keys() if self.values else []:
|
||||||
if self.model and self.model.__fields__[self.field].type_ == datetime:
|
clean_key = key.split("__")[0]
|
||||||
|
if self.model and self.model.__fields__[clean_key].type_ == datetime:
|
||||||
placeholder = compat_timestamp_placeholder(key)
|
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:
|
else:
|
||||||
stmt.append(f"{prefix}{self.field} {self.op.as_sql} :{key}")
|
placeholder = f":{key}"
|
||||||
|
stmt.append(f"{clean_key} {self.op.as_sql} {placeholder}")
|
||||||
if self.op in {Operator.INCLUDE, Operator.EXCLUDE}:
|
return " OR ".join(stmt)
|
||||||
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]):
|
class Filters(BaseModel, Generic[TFilterModel]):
|
||||||
@@ -570,14 +527,13 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
search: str | None = None
|
search: str | None = None
|
||||||
|
|
||||||
offset: int | None = None
|
offset: int | None = None
|
||||||
limit: int | None = 10
|
limit: int | None = None
|
||||||
|
|
||||||
sortby: str | None = None
|
sortby: str | None = None
|
||||||
direction: Literal["asc", "desc"] | None = None
|
direction: Literal["asc", "desc"] | None = None
|
||||||
|
|
||||||
model: type[TFilterModel] | None = None
|
model: type[TFilterModel] | None = None
|
||||||
|
|
||||||
table_name: str | None = None
|
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_sortby(cls, values):
|
def validate_sortby(cls, values):
|
||||||
sortby = values.get("sortby")
|
sortby = values.get("sortby")
|
||||||
@@ -592,8 +548,8 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
def pagination(self) -> str:
|
def pagination(self) -> str:
|
||||||
stmt = ""
|
stmt = ""
|
||||||
self.limit = self.limit or 10
|
if self.limit:
|
||||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
stmt += f"LIMIT {self.limit} "
|
||||||
if self.offset:
|
if self.offset:
|
||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
return stmt
|
return stmt
|
||||||
@@ -619,8 +575,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
|
|
||||||
def order_by(self) -> str:
|
def order_by(self) -> str:
|
||||||
if self.sortby:
|
if self.sortby:
|
||||||
prefix = f"{self.table_name}." if self.table_name else ""
|
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||||
return f"ORDER BY {prefix}{self.sortby} {self.direction or 'asc'}"
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def values(self, values: dict | None = None) -> dict:
|
def values(self, values: dict | None = None) -> dict:
|
||||||
@@ -632,26 +587,12 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
for key, value in page_filter.values.items():
|
for key, value in page_filter.values.items():
|
||||||
if page_filter.op == Operator.LIKE:
|
if page_filter.op == Operator.LIKE:
|
||||||
values[key] = f"%{value}%"
|
values[key] = f"%{value}%"
|
||||||
elif page_filter.op in {Operator.EVERY, Operator.ANY}:
|
|
||||||
values[key] = f"""%"{value}"%"""
|
|
||||||
else:
|
else:
|
||||||
values[key] = value
|
values[key] = value
|
||||||
if self.search and self.model:
|
if self.search and self.model:
|
||||||
values["search"] = f"%{self.search.lower()}%"
|
values["search"] = f"%{self.search.lower()}%"
|
||||||
return values
|
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:
|
def insert_query(table_name: str, model: BaseModel) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -707,7 +648,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
or type_ is dict
|
or type_ is dict
|
||||||
or get_origin(outertype_) is list
|
or get_origin(outertype_) is list
|
||||||
):
|
):
|
||||||
_dict[key] = json.dumps(value, cls=DbJsonEncoder)
|
_dict[key] = json.dumps(value)
|
||||||
continue
|
continue
|
||||||
_dict[key] = value
|
_dict[key] = value
|
||||||
|
|
||||||
@@ -781,11 +722,3 @@ def _safe_load_json(value: str) -> dict:
|
|||||||
# DB is corrupted if it gets here
|
# DB is corrupted if it gets here
|
||||||
logger.error(f"Failed to decode JSON: '{value}'")
|
logger.error(f"Failed to decode JSON: '{value}'")
|
||||||
return {}
|
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,28 +359,3 @@ def url_for_interceptor(original_method):
|
|||||||
# Upgraded extensions modify the path.
|
# Upgraded extensions modify the path.
|
||||||
# This interceptor ensures that the path is normalized.
|
# This interceptor ensures that the path is normalized.
|
||||||
Request.url_for = url_for_interceptor(Request.url_for) # type: ignore[method-assign]
|
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.",
|
|
||||||
)
|
|
||||||
|
|||||||
+17
-25
@@ -18,7 +18,6 @@ from pydantic.schema import field_schema
|
|||||||
from lnbits.jinja2_templating import Jinja2Templates
|
from lnbits.jinja2_templating import Jinja2Templates
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.crypto import AESCipher
|
from lnbits.utils.crypto import AESCipher
|
||||||
from lnbits.utils.exchange_rates import currencies
|
|
||||||
|
|
||||||
from .db import FilterModel
|
from .db import FilterModel
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
t.env.globals["static_url_for"] = static_url_for
|
t.env.globals["static_url_for"] = static_url_for
|
||||||
|
|
||||||
window_settings = {
|
window_settings = {
|
||||||
"AD_SPACE": settings.lnbits_ad_space,
|
"AD_SPACE": settings.lnbits_ad_space.split(","),
|
||||||
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
"AD_SPACE_ENABLED": settings.lnbits_ad_space_enabled,
|
||||||
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
"AD_SPACE_TITLE": settings.lnbits_ad_space_title,
|
||||||
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
"EXTENSIONS": list(settings.lnbits_installed_extensions_ids),
|
||||||
@@ -98,20 +97,15 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||||
"LNBITS_VERSION": settings.version,
|
"LNBITS_VERSION": settings.version,
|
||||||
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
"USE_CUSTOM_LOGO": settings.lnbits_custom_logo,
|
||||||
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
"USE_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||||
"LNBITS_DEFAULT_THEME": settings.lnbits_default_theme,
|
"USE_DEFAULT_THEME": settings.lnbits_default_theme,
|
||||||
"LNBITS_DEFAULT_BORDER": settings.lnbits_default_border,
|
"USE_DEFAULT_BORDER": settings.lnbits_default_border,
|
||||||
"LNBITS_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
"USE_DEFAULT_GRADIENT": settings.lnbits_default_gradient,
|
||||||
"LNBITS_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
"USE_DEFAULT_BGIMAGE": settings.lnbits_default_bgimage,
|
||||||
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
"VOIDWALLET": settings.lnbits_backend_wallet_class == "VoidWallet",
|
||||||
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
"WEBPUSH_PUBKEY": settings.lnbits_webpush_pubkey,
|
||||||
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
"LNBITS_DENOMINATION": settings.lnbits_denomination,
|
||||||
"has_holdinvoice": settings.has_holdinvoice,
|
"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
|
t.env.globals["WINDOW_SETTINGS"] = window_settings
|
||||||
@@ -204,11 +198,6 @@ def is_valid_username(username: str) -> bool:
|
|||||||
return re.fullmatch(username_regex, username) is not None
|
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:
|
def is_valid_external_id(external_id: str) -> bool:
|
||||||
if len(external_id) > 256:
|
if len(external_id) > 256:
|
||||||
return False
|
return False
|
||||||
@@ -364,6 +353,17 @@ def normalize_path(path: str | None) -> str:
|
|||||||
return "/" + "/".join(path_segments(path))
|
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:
|
def normalize_endpoint(endpoint: str, add_proto=True) -> str:
|
||||||
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
|
||||||
if add_proto:
|
if add_proto:
|
||||||
@@ -397,11 +397,3 @@ def is_snake_case(v: str) -> bool:
|
|||||||
|
|
||||||
def lowercase_first_letter(s: str) -> str:
|
def lowercase_first_letter(s: str) -> str:
|
||||||
return s[:1].lower() + s[1:] if s else s
|
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()
|
|
||||||
|
|||||||
+3
-13
@@ -280,11 +280,8 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
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=[
|
default=[
|
||||||
"image/png",
|
"image/png",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
@@ -300,12 +297,6 @@ class AssetSettings(LNbitsSettings):
|
|||||||
"heics",
|
"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):
|
class FeeSettings(LNbitsSettings):
|
||||||
@@ -438,7 +429,7 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
|
|
||||||
lnbits_notification_settings_update: bool = Field(default=True)
|
lnbits_notification_settings_update: bool = Field(default=True)
|
||||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||||
notification_balance_delta_threshold_sats: int = Field(default=1, ge=0)
|
notification_balance_delta_changed: bool = Field(default=True)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
@@ -876,7 +867,6 @@ class EditableSettings(
|
|||||||
ExtensionsSettings,
|
ExtensionsSettings,
|
||||||
ThemesSettings,
|
ThemesSettings,
|
||||||
OpsSettings,
|
OpsSettings,
|
||||||
AssetSettings,
|
|
||||||
FeeSettings,
|
FeeSettings,
|
||||||
ExchangeProvidersSettings,
|
ExchangeProvidersSettings,
|
||||||
SecuritySettings,
|
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,11 +185,6 @@ body[data-theme=salvador] [data-theme=salvador] .q-stepper--dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.gradient-bg {
|
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-image: linear-gradient(to bottom right, var(--q-dark-page), #0a0a0a);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
@@ -220,53 +215,6 @@ body.body--dark .q-drawer {
|
|||||||
backdrop-filter: blur(6px);
|
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] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -393,65 +341,3 @@ video {
|
|||||||
.q-dialog__inner--minimized {
|
.q-dialog__inner--minimized {
|
||||||
padding: 12px;
|
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,14 +25,12 @@ window.localisation.en = {
|
|||||||
reconnect: 'Reconnect',
|
reconnect: 'Reconnect',
|
||||||
open_channel: 'Open Channel',
|
open_channel: 'Open Channel',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
clear: 'Clear',
|
|
||||||
close_channel: 'Close Channel',
|
close_channel: 'Close Channel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
restart: 'Restart server',
|
restart: 'Restart server',
|
||||||
image_library: 'Image Library',
|
image_library: 'Image Library',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
save_tooltip: 'Save your changes',
|
save_tooltip: 'Save your changes',
|
||||||
must_save: 'You have unsaved changes',
|
|
||||||
credit_debit: 'Credit / Debit',
|
credit_debit: 'Credit / Debit',
|
||||||
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
credit_hint: 'Press Enter to credit/debit wallet (negative values allowed)',
|
||||||
credit_label: '{denomination} to credit/debit',
|
credit_label: '{denomination} to credit/debit',
|
||||||
@@ -54,17 +52,9 @@ window.localisation.en = {
|
|||||||
stored_paylinks: 'Stored LNURL pay links',
|
stored_paylinks: 'Stored LNURL pay links',
|
||||||
wallet: 'Wallet: ',
|
wallet: 'Wallet: ',
|
||||||
wallet_name: 'Wallet name',
|
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',
|
wallets: 'Wallets',
|
||||||
exclude_wallets: 'Exclude Wallets',
|
exclude_wallets: 'Exclude Wallets',
|
||||||
add_wallet: 'Add wallet',
|
add_wallet: 'Add wallet',
|
||||||
reject_wallet: 'Reject wallet',
|
|
||||||
add_new_wallet: 'Add a new wallet',
|
add_new_wallet: 'Add a new wallet',
|
||||||
pin_wallet: 'Pin wallet',
|
pin_wallet: 'Pin wallet',
|
||||||
delete_wallet: 'Delete wallet',
|
delete_wallet: 'Delete wallet',
|
||||||
@@ -151,8 +141,6 @@ window.localisation.en = {
|
|||||||
extensions: 'Extensions',
|
extensions: 'Extensions',
|
||||||
no_extensions: "You don't have any extensions installed :(",
|
no_extensions: "You don't have any extensions installed :(",
|
||||||
created: 'Created',
|
created: 'Created',
|
||||||
created_at: 'Created At',
|
|
||||||
updated_at: 'Updated At',
|
|
||||||
search_extensions: 'Search extensions',
|
search_extensions: 'Search extensions',
|
||||||
search_wallets: 'Search wallets',
|
search_wallets: 'Search wallets',
|
||||||
extension_sources: 'Extension Sources',
|
extension_sources: 'Extension Sources',
|
||||||
@@ -163,7 +151,6 @@ window.localisation.en = {
|
|||||||
repository: 'Repository',
|
repository: 'Repository',
|
||||||
confirm_continue: 'Are you sure you want to continue?',
|
confirm_continue: 'Are you sure you want to continue?',
|
||||||
manage_extension_details: 'Install/uninstall extension',
|
manage_extension_details: 'Install/uninstall extension',
|
||||||
upload: 'Upload',
|
|
||||||
install: 'Install',
|
install: 'Install',
|
||||||
uninstall: 'Uninstall',
|
uninstall: 'Uninstall',
|
||||||
drop_db: 'Remove Data',
|
drop_db: 'Remove Data',
|
||||||
@@ -319,7 +306,7 @@ window.localisation.en = {
|
|||||||
|
|
||||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||||
notification_balance_delta_changed_desc:
|
notification_balance_delta_changed_desc:
|
||||||
'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.',
|
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
|
||||||
|
|
||||||
enable_watchdog: 'Enable Watchdog Switch',
|
enable_watchdog: 'Enable Watchdog Switch',
|
||||||
enable_watchdog_desc:
|
enable_watchdog_desc:
|
||||||
@@ -412,8 +399,6 @@ window.localisation.en = {
|
|||||||
first_name: 'First Name',
|
first_name: 'First Name',
|
||||||
last_name: 'Last Name',
|
last_name: 'Last Name',
|
||||||
picture: 'Picture',
|
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',
|
verify_email: 'Verify email with',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
update_account: 'Update Account',
|
update_account: 'Update Account',
|
||||||
@@ -438,26 +423,6 @@ window.localisation.en = {
|
|||||||
toggle_gradient: 'Toggle Gradient',
|
toggle_gradient: 'Toggle Gradient',
|
||||||
gradient_background: 'Gradient Background',
|
gradient_background: 'Gradient Background',
|
||||||
language: 'Language',
|
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',
|
color_scheme: 'Color Scheme',
|
||||||
visible_wallet_count: 'Visible Wallet Count',
|
visible_wallet_count: 'Visible Wallet Count',
|
||||||
admin_settings: 'Admin Settings',
|
admin_settings: 'Admin Settings',
|
||||||
@@ -565,9 +530,7 @@ window.localisation.en = {
|
|||||||
reserve_fee_in_percent: 'Reserve fee in percent',
|
reserve_fee_in_percent: 'Reserve fee in percent',
|
||||||
payment_wait_time: 'Payment Wait Time (sec)',
|
payment_wait_time: 'Payment Wait Time (sec)',
|
||||||
payment_wait_time_desc:
|
payment_wait_time_desc:
|
||||||
'Wait time before marking an outgoing payment as pending. Default: 5s; raise for slow-settling invoices.',
|
'How long to wait when making a payment before marking it as pending. Set higher values for HODL invoices, Boltz, etc.',
|
||||||
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',
|
server_management: 'Server Management',
|
||||||
base_url: 'Base URL',
|
base_url: 'Base URL',
|
||||||
base_url_label: 'Static/Base url for the server',
|
base_url_label: 'Static/Base url for the server',
|
||||||
@@ -696,7 +659,6 @@ window.localisation.en = {
|
|||||||
view_list: 'View wallets as list',
|
view_list: 'View wallets as list',
|
||||||
view_column: 'View wallets as rows',
|
view_column: 'View wallets as rows',
|
||||||
filter_payments: 'Filter payments',
|
filter_payments: 'Filter payments',
|
||||||
filter_labels: 'Filter labels',
|
|
||||||
filter_date: 'Filter by date',
|
filter_date: 'Filter by date',
|
||||||
websocket_example: 'Websocket example',
|
websocket_example: 'Websocket example',
|
||||||
secret_key: 'Secret Key',
|
secret_key: 'Secret Key',
|
||||||
@@ -713,17 +675,6 @@ window.localisation.en = {
|
|||||||
funding_source_retries: 'Max Retries',
|
funding_source_retries: 'Max Retries',
|
||||||
funding_source_retries_desc:
|
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',
|
ws_connection_warning:
|
||||||
label: 'Label',
|
'Lost connection to the websocket. Wait while page tries to reconnect.'
|
||||||
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,46 +1,10 @@
|
|||||||
window.PageAccount = {
|
window.AccountPageLogic = {
|
||||||
template: '#page-account',
|
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
untouchedUser: null,
|
|
||||||
hasUsername: false,
|
hasUsername: false,
|
||||||
showUserId: false,
|
showUserId: false,
|
||||||
themeOptions: [
|
|
||||||
{
|
|
||||||
name: 'bitcoin',
|
|
||||||
color: 'deep-orange'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mint',
|
|
||||||
color: 'green'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'autumn',
|
|
||||||
color: 'brown'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'monochrome',
|
|
||||||
color: 'grey'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'salvador',
|
|
||||||
color: 'blue-10'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'freedom',
|
|
||||||
color: 'pink-13'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cyber',
|
|
||||||
color: 'light-green-9'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'flamingo',
|
|
||||||
color: 'pink-3'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
reactionOptions: [
|
reactionOptions: [
|
||||||
'None',
|
'None',
|
||||||
'confettiBothSides',
|
'confettiBothSides',
|
||||||
@@ -117,95 +81,13 @@ window.PageAccount = {
|
|||||||
allRead: false,
|
allRead: false,
|
||||||
allWrite: false
|
allWrite: false
|
||||||
},
|
},
|
||||||
assets: [],
|
|
||||||
assetsTable: {
|
|
||||||
loading: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('Name'),
|
|
||||||
field: 'name',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'created_at',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('created_at'),
|
|
||||||
field: 'created_at',
|
|
||||||
sortable: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 6,
|
|
||||||
page: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
assetsUploadToPublic: false,
|
|
||||||
notifications: {
|
notifications: {
|
||||||
nostr: {
|
nostr: {
|
||||||
identifier: ''
|
identifier: ''
|
||||||
}
|
}
|
||||||
},
|
|
||||||
labels: [],
|
|
||||||
labelsDialog: {
|
|
||||||
show: false,
|
|
||||||
data: {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
color: '#000000'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
labelsTable: {
|
|
||||||
loading: false,
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('Name'),
|
|
||||||
field: 'name',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('description'),
|
|
||||||
field: 'description'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'color',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('color'),
|
|
||||||
field: 'color'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
pagination: {
|
|
||||||
rowsPerPage: 6,
|
|
||||||
page: 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
'assetsTable.search': {
|
|
||||||
handler() {
|
|
||||||
const props = {}
|
|
||||||
if (this.assetsTable.search) {
|
|
||||||
props['search'] = this.assetsTable.search
|
|
||||||
}
|
|
||||||
this.getUserAssets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isUserTouched() {
|
|
||||||
return JSON.stringify(this.user) !== JSON.stringify(this.untouchedUser)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
@@ -228,7 +110,6 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
this.user = data
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
@@ -268,7 +149,6 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
this.user = data
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
this.credentialsData.show = false
|
this.credentialsData.show = false
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -291,7 +171,6 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
this.user = data
|
this.user = data
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
this.credentialsData.show = false
|
this.credentialsData.show = false
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
@@ -520,161 +399,6 @@ window.PageAccount = {
|
|||||||
} finally {
|
} finally {
|
||||||
this.apiAcl.password = ''
|
this.apiAcl.password = ''
|
||||||
}
|
}
|
||||||
},
|
|
||||||
async getUserAssets(props) {
|
|
||||||
try {
|
|
||||||
this.assetsTable.loading = true
|
|
||||||
const params = LNbits.utils.prepareFilterQuery(this.assetsTable, props)
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/api/v1/assets/paginated?${params}`,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
this.assets = data.data
|
|
||||||
this.assetsTable.pagination.rowsNumber = data.total
|
|
||||||
} catch (e) {
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
} finally {
|
|
||||||
this.assetsTable.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onImageInput(e) {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
this.uploadAsset(file)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async uploadAsset(file) {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'POST',
|
|
||||||
`/api/v1/assets?public_asset=${this.assetsUploadToPublic}`,
|
|
||||||
null,
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
headers: {'Content-Type': 'multipart/form-data'}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Upload successful!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
await this.getUserAssets()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteAsset(asset) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this asset?')
|
|
||||||
.onOk(async () => {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request(
|
|
||||||
'DELETE',
|
|
||||||
`/api/v1/assets/${asset.id}`,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Asset deleted.'
|
|
||||||
})
|
|
||||||
await this.getUserAssets()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async toggleAssetPublicAccess(asset) {
|
|
||||||
try {
|
|
||||||
await LNbits.api.request('PUT', `/api/v1/assets/${asset.id}`, null, {
|
|
||||||
is_public: !asset.is_public
|
|
||||||
})
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'Update successful!',
|
|
||||||
icon: null
|
|
||||||
})
|
|
||||||
await this.getUserAssets()
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
LNbits.utils.notifyApiError(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
copyAssetLinkToClipboard(asset) {
|
|
||||||
const assetUrl = `${window.location.origin}/api/v1/assets/${asset.id}/binary`
|
|
||||||
this.copyText(assetUrl)
|
|
||||||
},
|
|
||||||
addUserLabel() {
|
|
||||||
if (!this.labelsDialog.data.name) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Name is required.'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.labelsDialog.data.color) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'Color is required.'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.user.extra.labels = this.user.extra.labels || []
|
|
||||||
const duplicate = this.user.extra.labels.find(
|
|
||||||
label => label.name === this.labelsDialog.data.name
|
|
||||||
)
|
|
||||||
if (duplicate) {
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'warning',
|
|
||||||
message: 'A label with this name already exists.'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.user.extra.labels.unshift({...this.labelsDialog.data})
|
|
||||||
this.labelsDialog.show = false
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
openAddLabelDialog() {
|
|
||||||
this.labelsDialog.data = {
|
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
color: '#000000'
|
|
||||||
}
|
|
||||||
this.labelsDialog.show = true
|
|
||||||
},
|
|
||||||
openEditLabelDialog(label) {
|
|
||||||
this.labelsDialog.data = {
|
|
||||||
name: label.name,
|
|
||||||
description: label.description,
|
|
||||||
color: label.color
|
|
||||||
}
|
|
||||||
this.labelsDialog.show = true
|
|
||||||
},
|
|
||||||
updateUserLabel() {
|
|
||||||
const label = this.labelsDialog.data
|
|
||||||
const existingLabels = JSON.parse(JSON.stringify(this.user.extra.labels))
|
|
||||||
this.user.extra.labels = this.user.extra.labels.filter(
|
|
||||||
l => l.name !== label.name
|
|
||||||
)
|
|
||||||
const labelUpdated = this.addUserLabel()
|
|
||||||
if (!labelUpdated) {
|
|
||||||
this.user.extra.labels = existingLabels
|
|
||||||
}
|
|
||||||
this.labelsDialog.show = false
|
|
||||||
},
|
|
||||||
deleteUserLabel(label) {
|
|
||||||
LNbits.utils
|
|
||||||
.confirmDialog('Are you sure you want to delete this label?')
|
|
||||||
.onOk(() => {
|
|
||||||
this.user.extra.labels = this.user.extra.labels.filter(
|
|
||||||
l => l.name !== label.name
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -682,7 +406,6 @@ window.PageAccount = {
|
|||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.getAuthenticatedUser()
|
const {data} = await LNbits.api.getAuthenticatedUser()
|
||||||
this.user = data
|
this.user = data
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(data))
|
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
if (!this.user.extra) this.user.extra = {}
|
if (!this.user.extra) this.user.extra = {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -693,11 +416,5 @@ window.PageAccount = {
|
|||||||
this.tab = hash
|
this.tab = hash
|
||||||
}
|
}
|
||||||
await this.getApiACLs()
|
await this.getApiACLs()
|
||||||
await this.getUserAssets()
|
|
||||||
|
|
||||||
// filter out themes that are not allowed
|
|
||||||
this.themeOptions = this.themeOptions.filter(theme =>
|
|
||||||
this.LNBITS_THEME_OPTIONS.includes(theme.name)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,696 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
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]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
window.PageAudit = {
|
window.AuditPageLogic = {
|
||||||
template: '#page-audit',
|
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
+906
-22
@@ -1,8 +1,265 @@
|
|||||||
window.LNbits = {
|
window.LNbits = {
|
||||||
g: window.g,
|
g: window.g,
|
||||||
utils: window._lnbitsUtils,
|
api: {
|
||||||
api: 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) {
|
||||||
|
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) {
|
||||||
|
const reconnectInterval = 5000 // 5 seconds
|
||||||
|
const maxRetries = 12 // ~60 seconds
|
||||||
|
const websocketUrlFull = `${websocketUrl}/${wallet.inkey}`
|
||||||
|
|
||||||
|
if (!g.activeWebsockets) g.activeWebsockets = {}
|
||||||
|
if (!g.disconnectedWallets) g.disconnectedWallets = new Set()
|
||||||
|
|
||||||
|
const updateConnectionWarning = () => {
|
||||||
|
g.connectionWarning = g.disconnectedWallets.size > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const connect = (retryCount = 0) => {
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(websocketUrlFull)
|
||||||
|
g.activeWebsockets[wallet.id] = ws
|
||||||
|
console.debug(
|
||||||
|
`Connecting WebSocket for wallet ${wallet.name}... (attempt ${retryCount + 1})`
|
||||||
|
)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.debug(`WebSocket connected for wallet ${wallet.name}`)
|
||||||
|
g.disconnectedWallets.delete(wallet.id)
|
||||||
|
updateConnectionWarning()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = ev => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(ev.data)
|
||||||
|
if (data.payment) cb(data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing WebSocket message:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onerror = err => {
|
||||||
|
console.error(`WebSocket error for wallet ${wallet.name}:`, err)
|
||||||
|
ws.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
g.disconnectedWallets.add(wallet.id)
|
||||||
|
updateConnectionWarning()
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
console.warn(
|
||||||
|
`WebSocket closed for wallet ${wallet.name}. Reconnecting in ${reconnectInterval / 1000}s...`
|
||||||
|
)
|
||||||
|
setTimeout(() => connect(retryCount + 1), reconnectInterval)
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`WebSocket for wallet ${wallet.name} failed after ${maxRetries} attempts. Stopping retries.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`WebSocket connection failed for wallet ${wallet.name}:`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
g.disconnectedWallets.add(wallet.id)
|
||||||
|
updateConnectionWarning()
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
setTimeout(() => connect(retryCount + 1), reconnectInterval)
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[LNbits] Stopped retrying wallet ${wallet.name} after ${maxRetries} attempts.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect()
|
||||||
|
}
|
||||||
|
},
|
||||||
map: {
|
map: {
|
||||||
|
extension(data) {
|
||||||
|
const obj = {...data}
|
||||||
|
obj.url = ['/', obj.code, '/'].join('')
|
||||||
|
return obj
|
||||||
|
},
|
||||||
user(data) {
|
user(data) {
|
||||||
const obj = {
|
const obj = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
@@ -15,12 +272,16 @@ window.LNbits = {
|
|||||||
extra: data.extra ?? {}
|
extra: data.extra ?? {}
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
obj.wallets = obj.wallets
|
||||||
if (a.extra.pinned !== b.extra.pinned) {
|
.map(obj => {
|
||||||
return a.extra.pinned ? -1 : 1
|
return mapWallet(obj)
|
||||||
}
|
})
|
||||||
return a.name.localeCompare(b.name)
|
.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 => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
@@ -31,36 +292,659 @@ window.LNbits = {
|
|||||||
0,
|
0,
|
||||||
data.wallets.length - data.extra.visible_wallet_count
|
data.wallets.length - data.extra.visible_wallet_count
|
||||||
)
|
)
|
||||||
obj.walletInvitesCount = data.extra.wallet_invite_requests?.length || 0
|
|
||||||
return obj
|
return obj
|
||||||
},
|
},
|
||||||
wallet(data) {
|
wallet(data) {
|
||||||
newWallet = {
|
newWallet = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
walletType: data.wallet_type,
|
|
||||||
sharePermissions: data.share_permissions,
|
|
||||||
sharedWalletId: data.shared_wallet_id,
|
|
||||||
adminkey: data.adminkey,
|
adminkey: data.adminkey,
|
||||||
inkey: data.inkey,
|
inkey: data.inkey,
|
||||||
currency: data.currency,
|
currency: data.currency,
|
||||||
extra: data.extra,
|
extra: data.extra
|
||||||
canReceivePayments: true,
|
|
||||||
canSendPayments: true
|
|
||||||
}
|
}
|
||||||
newWallet.msat = data.balance_msat
|
newWallet.msat = data.balance_msat
|
||||||
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
newWallet.sat = Math.floor(data.balance_msat / 1000)
|
||||||
newWallet.fsat = new Intl.NumberFormat(window.i18n.global.locale).format(
|
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
|
||||||
newWallet.sat
|
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.url = `/wallet?&wal=${data.id}`
|
||||||
newWallet.storedPaylinks = data.stored_paylinks.links
|
|
||||||
return newWallet
|
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: '',
|
||||||
|
wsDisconnectionNotification: false,
|
||||||
|
wsReconnectedNotification: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
silentConnectionCleanup: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Retry timer reference (for cleanup)
|
||||||
|
this.connectionRetryTimer = setInterval(() => {
|
||||||
|
if (this.g.disconnectedWallets && this.g.disconnectedWallets.size > 0) {
|
||||||
|
console.warn('Some wallets are still disconnected. Retrying...')
|
||||||
|
this.paymentEvents()
|
||||||
|
}
|
||||||
|
}, 60000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
this.silentConnectionCleanup = true
|
||||||
|
// Prevent background timers when component is destroyed
|
||||||
|
if (this.connectionRetryTimer) {
|
||||||
|
clearInterval(this.connectionRetryTimer)
|
||||||
|
this.connectionRetryTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'g.connectionWarning': {
|
||||||
|
handler(newVal, oldVal) {
|
||||||
|
if (this.silentConnectionCleanup) return
|
||||||
|
|
||||||
|
if (newVal && !g.wsDisconnectionNotification) {
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Some wallet connections are lost. Retrying...',
|
||||||
|
color: 'negative',
|
||||||
|
icon: null,
|
||||||
|
position: 'top',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
g.wsDisconnectionNotification = true
|
||||||
|
g.wsReconnectedNotification = false
|
||||||
|
} else if (
|
||||||
|
oldVal === true &&
|
||||||
|
newVal === false &&
|
||||||
|
!g.wsReconnectedNotification
|
||||||
|
) {
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'All wallet connections restored.',
|
||||||
|
color: 'positive',
|
||||||
|
icon: null,
|
||||||
|
position: 'top',
|
||||||
|
timeout: 3000
|
||||||
|
})
|
||||||
|
g.wsReconnectedNotification = true
|
||||||
|
g.wsDisconnectionNotification = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+80
-275
@@ -21,26 +21,89 @@ 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', {
|
window.app.component('lnbits-manage', {
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
template: '#lnbits-manage',
|
template: '#lnbits-manage',
|
||||||
computed: {
|
props: ['showAdmin', 'showNode', 'showExtensions', 'showUsers', 'showAudit'],
|
||||||
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: {
|
methods: {
|
||||||
isActive(path) {
|
isActive(path) {
|
||||||
return window.location.pathname === path
|
return window.location.pathname === path
|
||||||
@@ -593,261 +656,3 @@ 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>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
window.app.component('lnbits-admin-server', {
|
|
||||||
props: ['form-data'],
|
|
||||||
template: '#lnbits-admin-server',
|
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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: {}
|
|
||||||
})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
window.app.component('lnbits-disclaimer', {
|
|
||||||
template: '#lnbits-disclaimer',
|
|
||||||
mixins: [window.windowMixin],
|
|
||||||
computed: {
|
|
||||||
showDisclaimer() {
|
|
||||||
return !g.disclaimerShown && g.isUserAuthorized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
window.app.component('lnbits-drawer', {
|
|
||||||
template: '#lnbits-drawer',
|
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
window.app.component('lnbits-admin-funding-sources', {
|
window.app.component('lnbits-funding-sources', {
|
||||||
template: '#lnbits-admin-funding-sources',
|
template: '#lnbits-funding-sources',
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
props: ['form-data', 'allowed-funding-sources'],
|
props: ['form-data', 'allowed-funding-sources'],
|
||||||
methods: {
|
methods: {
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
window.app.component('lnbits-header-wallets', {
|
|
||||||
template: '#lnbits-header-wallets',
|
|
||||||
mixins: [window.windowMixin]
|
|
||||||
})
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user