Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
962a09bb30 | ||
|
|
92c86eb837 | ||
|
|
dadff18a9c | ||
|
|
87c1684a63 | ||
|
|
91354f8be4 | ||
|
|
5f5d45e89f | ||
|
|
52bb125a25 | ||
|
|
2ca1ed897c |
@@ -44,15 +44,19 @@ jobs:
|
|||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -56,15 +56,19 @@ jobs:
|
|||||||
if: github.repository == 'lnbits/lnbits'
|
if: github.repository == 'lnbits/lnbits'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install dependencies for building secp256k1
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y build-essential automake libtool libffi-dev libgmp-dev
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Build and publish to pypi
|
- name: Set up Python 3.10
|
||||||
uses: JRubics/poetry-publish@v1.15
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
pypi_token: ${{ secrets.PYPI_API_KEY }}
|
python-version: "3.10"
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
- name: Build the project
|
||||||
|
run: uv build
|
||||||
|
- name: Publish to pypi
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_KEY }}
|
||||||
|
run: uv publish
|
||||||
|
|
||||||
appimage:
|
appimage:
|
||||||
needs: [ release ]
|
needs: [ release ]
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ from .middleware import (
|
|||||||
add_first_install_middleware,
|
add_first_install_middleware,
|
||||||
add_ip_block_middleware,
|
add_ip_block_middleware,
|
||||||
add_ratelimit_middleware,
|
add_ratelimit_middleware,
|
||||||
|
add_route_access_middleware,
|
||||||
)
|
)
|
||||||
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ def create_app() -> FastAPI:
|
|||||||
|
|
||||||
# adds security middleware
|
# adds security middleware
|
||||||
add_ip_block_middleware(app)
|
add_ip_block_middleware(app)
|
||||||
|
add_route_access_middleware(app)
|
||||||
add_ratelimit_middleware(app)
|
add_ratelimit_middleware(app)
|
||||||
|
|
||||||
register_exception_handlers(app)
|
register_exception_handlers(app)
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ async def get_user_from_account(
|
|||||||
super_user=account.is_super_user,
|
super_user=account.is_super_user,
|
||||||
fiat_providers=account.fiat_providers,
|
fiat_providers=account.fiat_providers,
|
||||||
has_password=account.password_hash is not None,
|
has_password=account.password_hash is not None,
|
||||||
|
ui_customization=account.ui_customization or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -848,3 +848,11 @@ async def m042_index_accounts(db: Connection):
|
|||||||
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
CREATE INDEX IF NOT EXISTS idx_accounts_{index} ON accounts ("{index}");
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m043_add_ui_customization_to_accounts(db: Connection):
|
||||||
|
"""
|
||||||
|
Adds ui_customization column to accounts.
|
||||||
|
Used for server side persistence of UI customization settings.
|
||||||
|
"""
|
||||||
|
await db.execute("ALTER TABLE accounts ADD COLUMN ui_customization TEXT")
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ class Account(AccountId):
|
|||||||
pubkey: str | None = None
|
pubkey: str | None = None
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
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))
|
||||||
@@ -288,6 +289,7 @@ class User(BaseModel):
|
|||||||
fiat_providers: list[str] = []
|
fiat_providers: list[str] = []
|
||||||
has_password: bool = False
|
has_password: bool = False
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
ui_customization: dict = Field(default_factory=dict)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
|||||||
return _auth_success_response(account.username, user_id, account.email)
|
return _auth_success_response(account.username, user_id, account.email)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/update")
|
@auth_router.patch("")
|
||||||
async def update(
|
async def update(
|
||||||
data: UpdateUser, account: Account = Depends(check_account_exists)
|
data: UpdateUser, account: Account = Depends(check_account_exists)
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
@@ -483,6 +483,22 @@ async def update(
|
|||||||
return await get_user_from_account(account)
|
return await get_user_from_account(account)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.patch("/ui")
|
||||||
|
async def update_ui_customization(
|
||||||
|
req: Request, account: Account = Depends(check_account_exists)
|
||||||
|
) -> Account:
|
||||||
|
ui_customization = await req.json()
|
||||||
|
account.ui_customization = {**(account.ui_customization or {}), **ui_customization}
|
||||||
|
|
||||||
|
if len(account.ui_customization or {}) > 1000 * 1024:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST, "UI customization too large. Drop some fields."
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_user_account(account)
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
@auth_router.put("/first_install")
|
@auth_router.put("/first_install")
|
||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates
|
|||||||
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
"LNBITS_SERVICE_FEE_WALLET": settings.lnbits_service_fee_wallet,
|
||||||
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
"LNBITS_SHOW_HOME_PAGE_ELEMENTS": settings.lnbits_show_home_page_elements,
|
||||||
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
"LNBITS_THEME_OPTIONS": settings.lnbits_theme_options,
|
||||||
|
"WALLET_FEATURED_BUTTON_LABEL": settings.lnbits_wallet_featured_button_label,
|
||||||
|
"WALLET_FEATURED_BUTTON_URL": settings.lnbits_wallet_featured_button_url,
|
||||||
|
"WALLET_FEATURED_BUTTON_ICON": settings.lnbits_wallet_featured_button_icon,
|
||||||
"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,
|
"LNBITS_DEFAULT_REACTION": settings.lnbits_default_reaction,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -18,6 +19,53 @@ from lnbits.core.models import AuditEntry
|
|||||||
from lnbits.helpers import normalize_path, template_renderer
|
from lnbits.helpers import normalize_path, template_renderer
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
_LOCALHOST_IPS = {"127.0.0.1", "::1"}
|
||||||
|
_PUBLIC_ASSET_PATHS = {
|
||||||
|
"/favicon.ico",
|
||||||
|
"/service-worker.js",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_match_path(path: str) -> str:
|
||||||
|
if not path:
|
||||||
|
return "/"
|
||||||
|
if path != "/" and path.endswith("/"):
|
||||||
|
return path.rstrip("/")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _route_pattern_matches(pattern: str, path: str) -> bool:
|
||||||
|
if not pattern:
|
||||||
|
return False
|
||||||
|
if not pattern.startswith("/"):
|
||||||
|
pattern = f"/{pattern}"
|
||||||
|
pattern = _normalize_match_path(pattern)
|
||||||
|
path = _normalize_match_path(path)
|
||||||
|
if pattern.endswith("*"):
|
||||||
|
prefix = pattern.rstrip("*")
|
||||||
|
return path.startswith(prefix)
|
||||||
|
if pattern == path:
|
||||||
|
return True
|
||||||
|
escaped = re.escape(pattern)
|
||||||
|
escaped = re.sub(r"\\\{[^/]+\\\}", r"[^/]+", escaped)
|
||||||
|
return re.fullmatch(escaped, path) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _response_by_accepted_type(request: Request, msg: str, status_code: HTTPStatus):
|
||||||
|
accept_header = request.headers.get("accept", "")
|
||||||
|
if "text/html" in accept_header.split(","):
|
||||||
|
return HTMLResponse(
|
||||||
|
status_code=status_code,
|
||||||
|
content=template_renderer()
|
||||||
|
.TemplateResponse(
|
||||||
|
request,
|
||||||
|
"error.html",
|
||||||
|
{"err": msg, "status_code": status_code, "message": msg},
|
||||||
|
)
|
||||||
|
.body,
|
||||||
|
)
|
||||||
|
return JSONResponse(status_code=status_code, content={"detail": msg})
|
||||||
|
|
||||||
|
|
||||||
class InstalledExtensionMiddleware:
|
class InstalledExtensionMiddleware:
|
||||||
# This middleware class intercepts calls made to the extensions API and:
|
# This middleware class intercepts calls made to the extensions API and:
|
||||||
@@ -235,6 +283,42 @@ def add_ip_block_middleware(app: FastAPI):
|
|||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
def add_route_access_middleware(app: FastAPI):
|
||||||
|
@app.middleware("http")
|
||||||
|
async def route_access_middleware(request: Request, call_next):
|
||||||
|
if not settings.lnbits_route_access_control_enabled:
|
||||||
|
return await call_next(request)
|
||||||
|
if not request.client:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
content={"detail": "No request client"},
|
||||||
|
)
|
||||||
|
if request.client.host in _LOCALHOST_IPS:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
path = request.url.path or "/"
|
||||||
|
if "/static/" in path or path in _PUBLIC_ASSET_PATHS:
|
||||||
|
return await call_next(request)
|
||||||
|
whitelist = settings.lnbits_route_access_whitelist
|
||||||
|
blacklist = settings.lnbits_route_access_blacklist
|
||||||
|
|
||||||
|
if whitelist:
|
||||||
|
if any(_route_pattern_matches(route, path) for route in whitelist):
|
||||||
|
return await call_next(request)
|
||||||
|
return _response_by_accepted_type(
|
||||||
|
request, f"Route not whitelisted: {path}", HTTPStatus.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
if blacklist and any(
|
||||||
|
_route_pattern_matches(route, path) for route in blacklist
|
||||||
|
):
|
||||||
|
return _response_by_accepted_type(
|
||||||
|
request, f"Route is blacklisted: {path}", HTTPStatus.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
def add_first_install_middleware(app: FastAPI):
|
def add_first_install_middleware(app: FastAPI):
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def first_install_middleware(request: Request, call_next):
|
async def first_install_middleware(request: Request, call_next):
|
||||||
|
|||||||
@@ -252,6 +252,11 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||||
|
|
||||||
|
lnbits_wallet_featured_button_label: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_url: str | None = Field(default=None)
|
||||||
|
lnbits_wallet_featured_button_icon: str | None = Field(default=None)
|
||||||
|
|
||||||
lnbits_custom_badge: str | None = Field(default=None)
|
lnbits_custom_badge: str | None = Field(default=None)
|
||||||
lnbits_custom_badge_color: str = Field(default="warning")
|
lnbits_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
@@ -416,6 +421,9 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||||
|
lnbits_route_access_control_enabled: bool = Field(default=False)
|
||||||
|
lnbits_route_access_whitelist: list[str] = Field(default=[])
|
||||||
|
lnbits_route_access_blacklist: list[str] = Field(default=[])
|
||||||
lnbits_callback_url_rules: list[str] = Field(
|
lnbits_callback_url_rules: list[str] = Field(
|
||||||
default=["https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"]
|
default=["https?://([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})(:\\d+)?"]
|
||||||
)
|
)
|
||||||
|
|||||||
+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
@@ -363,6 +363,18 @@ window.localisation.en = {
|
|||||||
watchdog: 'Watchdog',
|
watchdog: 'Watchdog',
|
||||||
server_logs: 'Server Logs',
|
server_logs: 'Server Logs',
|
||||||
ip_blocker: 'IP Blocker',
|
ip_blocker: 'IP Blocker',
|
||||||
|
route_access_control: 'Route Access Control',
|
||||||
|
route_access_control_enable: 'Enable route access control',
|
||||||
|
route_access_control_hint:
|
||||||
|
'When enabled, non-local requests are restricted. Requests from 127.0.0.1 (and ::1) are always allowed.',
|
||||||
|
route_access_whitelist_label: 'Route Whitelist',
|
||||||
|
route_access_whitelist_hint:
|
||||||
|
'Only allow non-local access to these routes (leave empty to disable whitelist mode).',
|
||||||
|
route_access_blacklist_label: 'Route Blacklist',
|
||||||
|
route_access_blacklist_hint:
|
||||||
|
'Block non-local access to these routes (ignored when a whitelist is set).',
|
||||||
|
route_access_save_confirm:
|
||||||
|
'Are you sure you want to save? This can impact access to LNbits if you are not accessing locally.',
|
||||||
security: 'Security',
|
security: 'Security',
|
||||||
security_tools: 'Security tools',
|
security_tools: 'Security tools',
|
||||||
block_access_hint: 'Block access by IP',
|
block_access_hint: 'Block access by IP',
|
||||||
@@ -658,6 +670,15 @@ window.localisation.en = {
|
|||||||
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
ui_site_description_hint: 'Use plain text, Markdown, or raw HTML',
|
||||||
ui_default_wallet_name: 'Default Wallet Name',
|
ui_default_wallet_name: 'Default Wallet Name',
|
||||||
ui_default_theme: 'Default Theme',
|
ui_default_theme: 'Default Theme',
|
||||||
|
wallet_featured_button_label: 'Wallet Featured Button Label',
|
||||||
|
wallet_featured_button_label_hint:
|
||||||
|
'Show featured button on the wallet homepage',
|
||||||
|
wallet_featured_button_url: 'Featured Button URL',
|
||||||
|
wallet_featured_button_url_hint:
|
||||||
|
'On click the button will open this URL. Leave empty to hide the button.',
|
||||||
|
wallet_featured_button_icon: 'Featured Button Icon',
|
||||||
|
wallet_featured_button_icon_hint:
|
||||||
|
'Icon shown on the featured button (check quasar icons)',
|
||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ window._lnbitsApi = {
|
|||||||
name: name
|
name: name
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
updateUiCustomization(data = {}) {
|
||||||
|
return this.request('patch', '/api/v1/auth/ui', null, data)
|
||||||
|
},
|
||||||
resetWalletKeys(wallet) {
|
resetWalletKeys(wallet) {
|
||||||
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
return this.request('put', `/api/v1/wallet/reset/${wallet.id}`).then(
|
||||||
res => {
|
res => {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ window.LNbits = {
|
|||||||
fiat_providers: data.fiat_providers || [],
|
fiat_providers: data.fiat_providers || [],
|
||||||
super_user: data.super_user,
|
super_user: data.super_user,
|
||||||
extra: data.extra ?? {},
|
extra: data.extra ?? {},
|
||||||
hasPassword: data.has_password ?? false
|
hasPassword: data.has_password ?? false,
|
||||||
|
uiCustomization: data.ui_customization || {}
|
||||||
}
|
}
|
||||||
const mapWallet = this.wallet
|
const mapWallet = this.wallet
|
||||||
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
obj.wallets = obj.wallets.map(mapWallet).sort((a, b) => {
|
||||||
|
|||||||
@@ -8,11 +8,58 @@ window.app.component('lnbits-admin-security', {
|
|||||||
serverlogEnabled: false,
|
serverlogEnabled: false,
|
||||||
nostrAcceptedUrl: '',
|
nostrAcceptedUrl: '',
|
||||||
formAllowedIPs: '',
|
formAllowedIPs: '',
|
||||||
formCallbackUrlRule: ''
|
formCallbackUrlRule: '',
|
||||||
|
routeOptions: [],
|
||||||
|
routeOptionsFiltered: [],
|
||||||
|
routeOptionsLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {},
|
created() {
|
||||||
|
this.loadOpenApiRoutes()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async loadOpenApiRoutes() {
|
||||||
|
this.routeOptionsLoading = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/openapi.json')
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load OpenAPI spec')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
const paths = Object.keys(data.paths || {}).sort()
|
||||||
|
this.routeOptions = paths
|
||||||
|
this.routeOptionsFiltered = paths
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
} finally {
|
||||||
|
this.routeOptionsLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addRouteOption(value, done) {
|
||||||
|
const route = value.trim()
|
||||||
|
if (!route) {
|
||||||
|
done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.routeOptions.includes(route)) {
|
||||||
|
this.routeOptions.push(route)
|
||||||
|
this.routeOptions.sort()
|
||||||
|
}
|
||||||
|
this.routeOptionsFiltered = this.routeOptions
|
||||||
|
done(route)
|
||||||
|
},
|
||||||
|
filterRouteOptions(val, update) {
|
||||||
|
update(() => {
|
||||||
|
if (!val) {
|
||||||
|
this.routeOptionsFiltered = this.routeOptions
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const needle = val.toLowerCase()
|
||||||
|
this.routeOptionsFiltered = this.routeOptions.filter(route =>
|
||||||
|
route.toLowerCase().includes(needle)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
addAllowedIPs() {
|
addAllowedIPs() {
|
||||||
const allowedIPs = this.formAllowedIPs.trim()
|
const allowedIPs = this.formAllowedIPs.trim()
|
||||||
const allowed_ips = this.formData.lnbits_allowed_ips
|
const allowed_ips = this.formData.lnbits_allowed_ips
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ window.app.component('lnbits-header', {
|
|||||||
window.location = '/users'
|
window.location = '/users'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e)
|
console.warn(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleLanguageChanged(lang) {
|
||||||
|
try {
|
||||||
|
await LNbits.api.updateUiCustomization({locale: lang.locale})
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Language Updated',
|
||||||
|
caption: lang.locale
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
window.app.component('lnbits-language-dropdown', {
|
window.app.component('lnbits-language-dropdown', {
|
||||||
template: '#lnbits-language-dropdown',
|
template: '#lnbits-language-dropdown',
|
||||||
|
computed: {
|
||||||
|
currentLanguage() {
|
||||||
|
return (
|
||||||
|
this.langs.find(lang => lang.value === window.i18n.global.locale) || {
|
||||||
|
value: 'en',
|
||||||
|
label: 'English',
|
||||||
|
display: '🇬🇧 EN'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
activeLanguage(lang) {
|
||||||
return window.i18n.global.locale === lang
|
return window.i18n.global.locale === lang
|
||||||
@@ -8,6 +19,7 @@ window.app.component('lnbits-language-dropdown', {
|
|||||||
this.g.locale = newValue
|
this.g.locale = newValue
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
|
this.$emit('language-changed', newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ window.app.component('lnbits-theme', {
|
|||||||
'g.disclaimerShown'(val) {
|
'g.disclaimerShown'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
this.$q.localStorage.setItem('lnbits.disclaimerShown', val)
|
||||||
},
|
},
|
||||||
|
'g.locale'(val) {
|
||||||
|
this.$q.localStorage.setItem('lnbits.lang', val)
|
||||||
|
window.i18n.global.locale = val
|
||||||
|
},
|
||||||
'g.isFiatPriority'(val) {
|
'g.isFiatPriority'(val) {
|
||||||
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
this.$q.localStorage.setItem('lnbits.isFiatPriority', val)
|
||||||
},
|
},
|
||||||
@@ -123,6 +127,13 @@ window.app.component('lnbits-theme', {
|
|||||||
if (this.g.mobileSimple === true) {
|
if (this.g.mobileSimple === true) {
|
||||||
document.body.classList.add('mobile-simple')
|
document.body.classList.add('mobile-simple')
|
||||||
}
|
}
|
||||||
|
Object.entries(this.g.user.uiCustomization || {}).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
if (key in this.g) {
|
||||||
|
this.g[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
this.checkUrlParams()
|
this.checkUrlParams()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ window.PageAccount = {
|
|||||||
color: 'pink-3'
|
color: 'pink-3'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
defaultSiteCustomisation: {
|
||||||
|
locale: 'en',
|
||||||
|
themeChoice: 'salvador',
|
||||||
|
bgimageChoice: '',
|
||||||
|
gradientChoice: true,
|
||||||
|
darkChoice: true,
|
||||||
|
borderChoice: 'hard-border',
|
||||||
|
reactionChoice: 'confettiBothSides'
|
||||||
|
},
|
||||||
reactionOptions: [
|
reactionOptions: [
|
||||||
'None',
|
'None',
|
||||||
'confettiBothSides',
|
'confettiBothSides',
|
||||||
@@ -213,26 +222,18 @@ window.PageAccount = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activeLanguage(lang) {
|
|
||||||
return window.i18n.global.locale === lang
|
|
||||||
},
|
|
||||||
changeLanguage(newValue) {
|
changeLanguage(newValue) {
|
||||||
window.i18n.global.locale = newValue
|
window.i18n.global.locale = newValue
|
||||||
this.$q.localStorage.set('lnbits.lang', newValue)
|
this.$q.localStorage.set('lnbits.lang', newValue)
|
||||||
},
|
},
|
||||||
async updateAccount() {
|
async updateAccount() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request('PATCH', '/api/v1/auth', null, {
|
||||||
'PUT',
|
user_id: this.g.user.id,
|
||||||
'/api/v1/auth/update',
|
username: this.g.user.username,
|
||||||
null,
|
email: this.g.user.email,
|
||||||
{
|
extra: this.g.user.extra
|
||||||
user_id: this.g.user.id,
|
})
|
||||||
username: this.g.user.username,
|
|
||||||
email: this.g.user.email,
|
|
||||||
extra: this.g.user.extra
|
|
||||||
}
|
|
||||||
)
|
|
||||||
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
this.untouchedUser = JSON.parse(JSON.stringify(this.g.user))
|
||||||
this.hasUsername = !!data.username
|
this.hasUsername = !!data.username
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
@@ -680,6 +681,23 @@ window.PageAccount = {
|
|||||||
l => l.name !== label.name
|
l => l.name !== label.name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async siteCustomisationChanged(options = {}) {
|
||||||
|
try {
|
||||||
|
Object.entries(options || {}).forEach(([key, value]) => {
|
||||||
|
if (key in this.g) {
|
||||||
|
this.g[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await LNbits.api.updateUiCustomization(options)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'UI Customization updated.'
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ window.PageAdmin = {
|
|||||||
lnbits_exchange_rate_providers: [],
|
lnbits_exchange_rate_providers: [],
|
||||||
lnbits_audit_exclude_paths: [],
|
lnbits_audit_exclude_paths: [],
|
||||||
lnbits_audit_include_paths: [],
|
lnbits_audit_include_paths: [],
|
||||||
lnbits_audit_http_response_codes: []
|
lnbits_audit_http_response_codes: [],
|
||||||
|
lnbits_route_access_whitelist: [],
|
||||||
|
lnbits_route_access_blacklist: []
|
||||||
},
|
},
|
||||||
isSuperUser: false,
|
isSuperUser: false,
|
||||||
needsRestart: false
|
needsRestart: false
|
||||||
@@ -69,6 +71,25 @@ window.PageAdmin = {
|
|||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
updateSettings() {
|
updateSettings() {
|
||||||
|
if (this.shouldConfirmRouteAccess()) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(this.$t('route_access_save_confirm'))
|
||||||
|
.onOk(() => this.persistSettings())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.persistSettings()
|
||||||
|
},
|
||||||
|
shouldConfirmRouteAccess() {
|
||||||
|
const fields = [
|
||||||
|
'lnbits_route_access_control_enabled',
|
||||||
|
'lnbits_route_access_whitelist',
|
||||||
|
'lnbits_route_access_blacklist'
|
||||||
|
]
|
||||||
|
return fields.some(
|
||||||
|
field => !_.isEqual(this.settings[field], this.formData[field])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
persistSettings() {
|
||||||
const data = _.omit(this.formData, [
|
const data = _.omit(this.formData, [
|
||||||
'is_super_user',
|
'is_super_user',
|
||||||
'lnbits_allowed_funding_sources',
|
'lnbits_allowed_funding_sources',
|
||||||
|
|||||||
@@ -296,6 +296,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-12">
|
||||||
|
<p v-text="$t('route_access_control')"></p>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-toggle
|
||||||
|
v-model="formData.lnbits_route_access_control_enabled"
|
||||||
|
:label="$t('route_access_control_enable')"
|
||||||
|
></q-toggle>
|
||||||
|
<div
|
||||||
|
class="text-caption text-grey-6 q-mt-xs"
|
||||||
|
v-text="$t('route_access_control_hint')"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
use-input
|
||||||
|
input-debounce="0"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
@new-value="addRouteOption"
|
||||||
|
:options="routeOptionsFiltered"
|
||||||
|
:loading="routeOptionsLoading"
|
||||||
|
@filter="filterRouteOptions"
|
||||||
|
v-model="formData.lnbits_route_access_whitelist"
|
||||||
|
:label="$t('route_access_whitelist_label')"
|
||||||
|
:hint="$t('route_access_whitelist_hint')"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
use-chips
|
||||||
|
use-input
|
||||||
|
input-debounce="0"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
@new-value="addRouteOption"
|
||||||
|
:options="routeOptionsFiltered"
|
||||||
|
:loading="routeOptionsLoading"
|
||||||
|
@filter="filterRouteOptions"
|
||||||
|
v-model="formData.lnbits_route_access_blacklist"
|
||||||
|
:label="$t('route_access_blacklist_label')"
|
||||||
|
:hint="$t('route_access_blacklist_hint')"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-12">
|
<div class="col-12 col-md-12">
|
||||||
<p v-text="$t('rate_limiter')"></p>
|
<p v-text="$t('rate_limiter')"></p>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
|
|||||||
@@ -86,6 +86,42 @@
|
|||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<p>
|
||||||
|
<span v-text="$t('wallet_featured_button_label')"></span>
|
||||||
|
</p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="formData.lnbits_wallet_featured_button_label"
|
||||||
|
label="Loop to Onchain"
|
||||||
|
:hint="$t('wallet_featured_button_label_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<p><span v-text="$t('wallet_featured_button_url')"></span></p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="formData.lnbits_wallet_featured_button_url"
|
||||||
|
label="/boltz"
|
||||||
|
:hint="$t('wallet_featured_button_url_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<p><span v-text="$t('wallet_featured_button_icon')"></span></p>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
type="text"
|
||||||
|
v-model="formData.lnbits_wallet_featured_button_icon"
|
||||||
|
label="bolt"
|
||||||
|
:hint="$t('wallet_featured_button_icon_hint')"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row q-col-gutter-md q-mt-md">
|
<div class="row q-col-gutter-md q-mt-md">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
<p><span v-text="$t('ui_custom_badge')"></span></p>
|
||||||
|
|||||||
@@ -71,8 +71,11 @@
|
|||||||
<span>OFFLINE</span>
|
<span>OFFLINE</span>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
|
|
||||||
<lnbits-language-dropdown></lnbits-language-dropdown>
|
<lnbits-language-dropdown
|
||||||
<q-btn-dropdown v-if="g.user" flat rounded size="sm" class="q-pl-sm">
|
@language-changed="handleLanguageChanged({locale: $event})"
|
||||||
|
></lnbits-language-dropdown>
|
||||||
|
|
||||||
|
<q-btn-dropdown v-if="g.user" flat rounded size="md" class="q-pl-sm">
|
||||||
<template v-slot:label>
|
<template v-slot:label>
|
||||||
<q-avatar
|
<q-avatar
|
||||||
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
|
v-if="g.user?.extra?.picture && g.user?.extra?.picture !== ''"
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template id="lnbits-language-dropdown">
|
<template id="lnbits-language-dropdown">
|
||||||
<q-btn-dropdown dense flat rounded size="sm" icon="language" class="q-pl-md">
|
<q-btn-dropdown dense flat rounded size="md" class="q-pl-md">
|
||||||
|
<template v-slot:label>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="currentLanguage.display"></q-item-label>
|
||||||
|
<q-tooltip><span v-text="currentLanguage.label"></span></q-tooltip>
|
||||||
|
</q-item-section>
|
||||||
|
</template>
|
||||||
<q-list v-for="(lang, index) in langs" :key="index">
|
<q-list v-for="(lang, index) in langs" :key="index">
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
|
|||||||
@@ -333,6 +333,16 @@
|
|||||||
class="q-mb-md"
|
class="q-mb-md"
|
||||||
>
|
>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="g.user.extra.visible_wallet_count"
|
||||||
|
:label="$t('visible_wallet_count')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
type="number"
|
||||||
|
class="q-mb-md"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="g.user.external_id"
|
v-model="g.user.external_id"
|
||||||
:label="$t('external_id')"
|
:label="$t('external_id')"
|
||||||
@@ -380,22 +390,11 @@
|
|||||||
<span v-text="$t('language')"></span>
|
<span v-text="$t('language')"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<lnbits-language-dropdown />
|
<lnbits-language-dropdown
|
||||||
</div>
|
@language-changed="
|
||||||
</div>
|
siteCustomisationChanged({locale: $event})
|
||||||
<div class="row q-mb-md">
|
"
|
||||||
<div class="col-4">
|
/>
|
||||||
<span v-text="$t('visible_wallet_count')"></span>
|
|
||||||
</div>
|
|
||||||
<div class="col-8">
|
|
||||||
<q-input
|
|
||||||
v-model="g.user.extra.visible_wallet_count"
|
|
||||||
:label="$t('visible_wallet_count')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
type="number"
|
|
||||||
class="q-mb-md"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -407,7 +406,9 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
v-for="theme in themeOptions"
|
v-for="theme in themeOptions"
|
||||||
:key="theme.name"
|
:key="theme.name"
|
||||||
@click="g.themeChoice = theme.name"
|
@click="
|
||||||
|
siteCustomisationChanged({themeChoice: theme.name})
|
||||||
|
"
|
||||||
:color="theme.color"
|
:color="theme.color"
|
||||||
dense
|
dense
|
||||||
flat
|
flat
|
||||||
@@ -427,6 +428,9 @@
|
|||||||
<q-input
|
<q-input
|
||||||
v-model="g.bgimageChoice"
|
v-model="g.bgimageChoice"
|
||||||
:label="$t('background_image')"
|
:label="$t('background_image')"
|
||||||
|
@update:model-value="
|
||||||
|
siteCustomisationChanged({bgimageChoice: $event})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('background_image')"></span
|
><span v-text="$t('background_image')"></span
|
||||||
@@ -445,6 +449,9 @@
|
|||||||
round
|
round
|
||||||
icon="gradient"
|
icon="gradient"
|
||||||
v-model="g.gradientChoice"
|
v-model="g.gradientChoice"
|
||||||
|
@update:model-value="
|
||||||
|
siteCustomisationChanged({gradientChoice: $event})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('toggle_gradient')"></span
|
><span v-text="$t('toggle_gradient')"></span
|
||||||
@@ -463,6 +470,9 @@
|
|||||||
flat
|
flat
|
||||||
round
|
round
|
||||||
v-model="g.darkChoice"
|
v-model="g.darkChoice"
|
||||||
|
@update:model-value="
|
||||||
|
siteCustomisationChanged({darkChoice: $event})
|
||||||
|
"
|
||||||
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
|
:icon="$q.dark.isActive ? 'brightness_3' : 'wb_sunny'"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
@@ -481,6 +491,9 @@
|
|||||||
v-model="g.borderChoice"
|
v-model="g.borderChoice"
|
||||||
:options="borderOptions"
|
:options="borderOptions"
|
||||||
label="Borders"
|
label="Borders"
|
||||||
|
@update:model-value="
|
||||||
|
siteCustomisationChanged({borderChoice: $event})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('border_choices')"></span
|
><span v-text="$t('border_choices')"></span
|
||||||
@@ -508,6 +521,9 @@
|
|||||||
v-model="g.reactionChoice"
|
v-model="g.reactionChoice"
|
||||||
:options="reactionOptions"
|
:options="reactionOptions"
|
||||||
label="Reactions"
|
label="Reactions"
|
||||||
|
@update:model-value="
|
||||||
|
siteCustomisationChanged({reactionChoice: $event})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
><span v-text="$t('payment_reactions')"></span
|
><span v-text="$t('payment_reactions')"></span
|
||||||
@@ -515,6 +531,17 @@
|
|||||||
</q-select>
|
</q-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn
|
||||||
|
@click="
|
||||||
|
siteCustomisationChanged(defaultSiteCustomisation)
|
||||||
|
"
|
||||||
|
:label="$t('reset_defaults')"
|
||||||
|
filled
|
||||||
|
color="primary"
|
||||||
|
class="float-right q-mb-md"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-section>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
<q-tab-panel name="notifications">
|
<q-tab-panel name="notifications">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|||||||
@@ -171,6 +171,19 @@
|
|||||||
><span v-text="$t('camera_tooltip')"></span
|
><span v-text="$t('camera_tooltip')"></span
|
||||||
></q-tooltip>
|
></q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<div
|
||||||
|
v-if="WALLET_FEATURED_BUTTON_URL"
|
||||||
|
class="float-right q-mt-sm q-ml-sm"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
:label="WALLET_FEATURED_BUTTON_LABEL"
|
||||||
|
:icon="WALLET_FEATURED_BUTTON_ICON || undefined"
|
||||||
|
size="sm"
|
||||||
|
:to="WALLET_FEATURED_BUTTON_URL"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
<lnbits-update-balance
|
<lnbits-update-balance
|
||||||
v-if="$q.screen.gt.md"
|
v-if="$q.screen.gt.md"
|
||||||
:wallet_id="this.g.wallet.id"
|
:wallet_id="this.g.wallet.id"
|
||||||
|
|||||||
Generated
+1
-1
@@ -4638,4 +4638,4 @@ migration = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "6c17d723baa390c36ecf6f861166878f2619acd47f878cf9253c5614ba25b9ea"
|
content-hash = "f63a5be62359f837513e01a8b0e5282a3b4509ea97439ba0c8226ccf3c158ceb"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ readme = "README.md"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bech32==1.2.0",
|
"bech32==1.2.0",
|
||||||
"click==8.3.1",
|
"click==8.3.1",
|
||||||
"ecdsa==0.19.1",
|
|
||||||
"fastapi==0.116.1",
|
"fastapi==0.116.1",
|
||||||
"starlette==0.47.1",
|
"starlette==0.47.1",
|
||||||
"httpx==0.27.2",
|
"httpx==0.27.2",
|
||||||
|
|||||||
@@ -2016,9 +2016,7 @@ async def test_api_update_user_labels(http_client: AsyncClient):
|
|||||||
]
|
]
|
||||||
data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra)
|
data = UpdateUser(user_id=user.id, username=f"u{tiny_id}", extra=user.extra)
|
||||||
assert data.extra
|
assert data.extra
|
||||||
response = await http_client.put(
|
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict())
|
||||||
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
assert len(user_data["extra"]["labels"]) == 2
|
assert len(user_data["extra"]["labels"]) == 2
|
||||||
@@ -2028,18 +2026,14 @@ async def test_api_update_user_labels(http_client: AsyncClient):
|
|||||||
assert user_data["extra"]["labels"][1]["color"] == "#00FF00"
|
assert user_data["extra"]["labels"][1]["color"] == "#00FF00"
|
||||||
|
|
||||||
data.extra.labels = []
|
data.extra.labels = []
|
||||||
response = await http_client.put(
|
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=data.dict())
|
||||||
"/api/v1/auth/update?usr=" + user.id, json=data.dict()
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
assert len(user_data["extra"]["labels"]) == 0
|
assert len(user_data["extra"]["labels"]) == 0
|
||||||
|
|
||||||
json_data = data.dict()
|
json_data = data.dict()
|
||||||
json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]}
|
json_data["extra"] = {"labels": [{"name": "label + 01", "color": "#FF0000"}]}
|
||||||
response = await http_client.put(
|
response = await http_client.patch("/api/v1/auth?usr=" + user.id, json=json_data)
|
||||||
"/api/v1/auth/update?usr=" + user.id, json=json_data
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|||||||
@@ -1256,7 +1256,6 @@ dependencies = [
|
|||||||
{ name = "bech32" },
|
{ name = "bech32" },
|
||||||
{ name = "bolt11" },
|
{ name = "bolt11" },
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "ecdsa" },
|
|
||||||
{ name = "embit" },
|
{ name = "embit" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "fastapi-sso" },
|
{ name = "fastapi-sso" },
|
||||||
@@ -1340,7 +1339,6 @@ requires-dist = [
|
|||||||
{ name = "breez-sdk", marker = "extra == 'breez'", specifier = "==0.8.0" },
|
{ name = "breez-sdk", marker = "extra == 'breez'", specifier = "==0.8.0" },
|
||||||
{ name = "breez-sdk-liquid", marker = "extra == 'breez'", specifier = "==0.11.11" },
|
{ name = "breez-sdk-liquid", marker = "extra == 'breez'", specifier = "==0.11.11" },
|
||||||
{ name = "click", specifier = "==8.3.1" },
|
{ name = "click", specifier = "==8.3.1" },
|
||||||
{ name = "ecdsa", specifier = "==0.19.1" },
|
|
||||||
{ name = "embit", specifier = "==0.8.0" },
|
{ name = "embit", specifier = "==0.8.0" },
|
||||||
{ name = "fastapi", specifier = "==0.116.1" },
|
{ name = "fastapi", specifier = "==0.116.1" },
|
||||||
{ name = "fastapi-sso", specifier = "==0.19.0" },
|
{ name = "fastapi-sso", specifier = "==0.19.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user