lnbits-legend/lnbits/app.py

630 lines
21 KiB
Python
Raw Normal View History

import asyncio
import glob
2021-05-07 04:22:02 +02:00
import importlib
import logging
import os
2023-01-18 15:25:44 +01:00
import shutil
import signal
import sys
2021-05-07 04:22:02 +02:00
import traceback
from hashlib import sha256
2022-06-13 22:43:17 +02:00
from http import HTTPStatus
from pathlib import Path
from typing import Callable, List, Optional
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from loguru import logger
from slowapi import Limiter
from slowapi.util import get_remote_address
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse
2024-02-09 13:10:51 +01:00
from lnbits.core.crud import get_dbversions, get_installed_extensions
2023-01-18 17:35:02 +01:00
from lnbits.core.helpers import migrate_extension_database
from lnbits.core.services import websocketUpdater
from lnbits.core.tasks import ( # watchdog_task
killswitch_task,
wait_for_paid_invoices,
)
from lnbits.settings import settings
from lnbits.tasks import (
cancel_all_tasks,
create_permanent_task,
register_invoice_listener,
)
from lnbits.utils.cache import cache
from lnbits.wallets import get_wallet_class, set_wallet_class
2024-02-09 13:10:51 +01:00
from .commands import migrate_databases
from .core import init_core_routers
from .core.db import core_app_extra
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
from .core.services import check_admin_settings, check_webpush_settings
from .core.views.api import add_installed_extension
from .core.views.generic import update_installed_extension_state
from .extension_manager import (
Extension,
InstallableExtension,
get_valid_extensions,
version_parse,
)
from .helpers import template_renderer
from .middleware import (
CustomGZipMiddleware,
ExtensionsRedirectMiddleware,
InstalledExtensionMiddleware,
add_first_install_middleware,
add_ip_block_middleware,
add_ratelimit_middleware,
)
from .requestvars import g
2021-10-17 19:33:29 +02:00
from .tasks import (
check_pending_payments,
internal_invoice_listener,
invoice_listener,
)
def create_app() -> FastAPI:
2022-07-07 16:24:36 +02:00
configure_logger()
2022-07-27 19:20:36 +02:00
app = FastAPI(
2023-08-24 11:52:12 +02:00
title=settings.lnbits_title,
description=(
"API for LNbits, the free and open source bitcoin wallet and "
"accounts system with plugins."
),
version=settings.version,
2022-07-27 19:20:36 +02:00
license_info={
"name": "MIT License",
2022-12-05 12:18:59 +01:00
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
2022-07-27 19:20:36 +02:00
},
)
2022-09-22 11:47:24 +02:00
# Allow registering new extensions routes without direct access to the `app` object
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
# register static files
static_path = Path("lnbits", "static")
static = StaticFiles(directory=static_path)
app.mount("/static", static, name="static")
2022-11-24 11:35:03 +01:00
g().base_url = f"http://{settings.host}:{settings.port}"
app.add_middleware(
2022-10-03 22:14:07 +02:00
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
app.add_middleware(
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
)
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
# required for SSO login
app.add_middleware(SessionMiddleware, secret_key=settings.auth_secret_key)
# order of these two middlewares is important
2022-11-29 17:19:33 +01:00
app.add_middleware(InstalledExtensionMiddleware)
app.add_middleware(ExtensionsRedirectMiddleware)
register_custom_extensions_path()
add_first_install_middleware(app)
# adds security middleware
add_ip_block_middleware(app)
add_ratelimit_middleware(app)
register_startup(app)
2020-09-28 04:12:55 +02:00
register_async_tasks(app)
register_exception_handlers(app)
register_shutdown(app)
return app
2022-09-22 10:46:11 +02:00
async def check_funding_source() -> None:
2022-10-06 09:38:17 +02:00
original_sigint_handler = signal.getsignal(signal.SIGINT)
2022-09-22 10:46:11 +02:00
def signal_handler(signal, frame):
logger.debug(
f"SIGINT received, terminating LNbits. signal: {signal}, frame: {frame}"
)
sys.exit(1)
2022-10-12 13:08:59 +02:00
signal.signal(signal.SIGINT, signal_handler)
2022-10-05 09:46:59 +02:00
WALLET = get_wallet_class()
# fallback to void after 30 seconds of failures
sleep_time = 5
timeout = int(30 / sleep_time)
balance = 0
retry_counter = 0
while True:
try:
error_message, balance = await WALLET.status()
if not error_message:
retry_counter = 0
break
logger.error(
f"The backend for {WALLET.__class__.__name__} isn't "
f"working properly: '{error_message}'",
RuntimeWarning,
)
Wallets: add cln-rest (#1775) * receive and pay works * fix linter issues * import Paymentstatus from core.models * fix test real payment * fix get_payment_status check in lnbits * fix tests? * simplify * refactor AsyncClient * inline import of get_wallet_class fixes the previous cyclic import * invoice stream working * add notes as a reminder to get rid of labels when cln-rest supports payment_hash * create Payment dummy classmethod * remove unnecessary fields from dummy * fixes tests? * fix model * fix cln bug (#1814) * auth header * rename cln to corelightning * add clnrest to admin_ui * add to clnrest allowed sources * add allowed sources to .env.example * allow macaroon files * add corelightning rest to workflow * proper env names * cleanup routine * log wallet connection errors and fix macaroon clnrest * print error on connection fails * clnrest: handle disconnects faster * fix test use of get_payment_status * make format * clnrest: add unhashed_description * add unhashed_description to test * description_hash test * unhashed_description not supported by clnrest * fix checking_id return in api_payments_create_invoice * refactor test to use client instead of api_payments * formatting, some errorlogging * fix test 1 * fix other tests, paid statuses was missing * error handling * revert unnecessary changes (#1854) * apply review of motorina0 --------- Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: jackstar12 <62219658+jackstar12@users.noreply.github.com> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-08-23 08:59:39 +02:00
except Exception as e:
logger.error(f"Error connecting to {WALLET.__class__.__name__}: {e}")
pass
if settings.lnbits_admin_ui and retry_counter == timeout:
set_void_wallet_class()
WALLET = get_wallet_class()
break
else:
logger.warning(f"Retrying connection to backend in {sleep_time} seconds...")
retry_counter += 1
await asyncio.sleep(sleep_time)
signal.signal(signal.SIGINT, original_sigint_handler)
logger.success(
f"✔️ Backend {WALLET.__class__.__name__} connected "
f"and with a balance of {balance} msat."
)
def set_void_wallet_class():
logger.warning(
"Fallback to VoidWallet, because the backend for "
f"{settings.lnbits_backend_wallet_class} isn't working properly"
)
set_wallet_class("VoidWallet")
async def check_installed_extensions(app: FastAPI):
"""
Check extensions that have been installed, but for some reason no longer present in
the 'lnbits/extensions' directory. One reason might be a docker-container that was
re-created. The 'data' directory (where the '.zip' files live) is expected to
persist state. Zips that are missing will be re-downloaded.
"""
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
installed_extensions = await build_all_installed_extensions_list(False)
2023-01-18 15:25:44 +01:00
for ext in installed_extensions:
try:
installed = await check_installed_extension_files(ext)
2023-01-18 17:35:02 +01:00
if not installed:
await restore_installed_extension(app, ext)
logger.info(
"✔️ Successfully re-installed extension: "
f"{ext.id} ({ext.installed_version})"
)
2023-01-23 10:51:53 +01:00
except Exception as e:
logger.warning(e)
logger.warning(
f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
)
logger.info(f"Installed Extensions ({len(installed_extensions)}):")
for ext in installed_extensions:
logger.info(f"{ext.id} ({ext.installed_version})")
async def build_all_installed_extensions_list(
include_deactivated: Optional[bool] = True,
) -> List[InstallableExtension]:
"""
Returns a list of all the installed extensions plus the extensions that
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
"""
installed_extensions = await get_installed_extensions()
installed_extensions_ids = [e.id for e in installed_extensions]
for ext_id in settings.lnbits_extensions_default_install:
if ext_id in installed_extensions_ids:
continue
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
ext_releases = sorted(
ext_releases, key=lambda r: version_parse(r.version), reverse=True
)
release = next((e for e in ext_releases if e.is_version_compatible), None)
if release:
ext_info = InstallableExtension(
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
)
installed_extensions.append(ext_info)
if include_deactivated:
return installed_extensions
if settings.lnbits_extensions_deactivate_all:
return []
return [
e
for e in installed_extensions
if e.id not in settings.lnbits_deactivated_extensions
]
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
2023-01-25 14:00:39 +01:00
if ext.has_installed_version:
return True
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
2023-01-18 15:25:44 +01:00
2023-03-20 13:20:52 +01:00
if f"./{str(ext.zip_path)}" not in zip_files:
await ext.download_archive()
ext.extract_archive()
2023-01-20 14:59:44 +01:00
return False
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
await add_installed_extension(ext)
await update_installed_extension_state(ext_id=ext.id, active=True)
extension = Extension.from_installable_ext(ext)
register_ext_routes(app, extension)
2024-02-09 13:10:51 +01:00
current_version = (await get_dbversions()).get(ext.id, 0)
await migrate_extension_database(extension, current_version)
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
2023-01-25 14:00:39 +01:00
if extension.upgrade_hash:
ext.nofiy_upgrade()
def register_routes(app: FastAPI) -> None:
"""Register FastAPI routes / LNbits extensions."""
init_core_routers(app)
for ext in get_valid_extensions(False):
try:
register_ext_routes(app, ext)
except Exception as e:
logger.error(f"Could not load extension `{ext.code}`: {str(e)}")
def register_custom_extensions_path():
if settings.has_default_extension_path:
return
default_ext_path = os.path.join("lnbits", "extensions")
if os.path.isdir(default_ext_path) and len(os.listdir(default_ext_path)) != 0:
logger.warning(
"You are using a custom extensions path, "
+ "but the default extensions directory is not empty. "
+ f"Please clean-up the '{default_ext_path}' directory."
)
logger.warning(
f"You can move the existing '{default_ext_path}' directory to: "
+ f" '{settings.lnbits_extensions_path}/extensions'"
)
sys.path.append(str(Path(settings.lnbits_extensions_path, "extensions")))
sys.path.append(str(Path(settings.lnbits_extensions_path, "upgrades")))
def register_new_ext_routes(app: FastAPI) -> Callable:
# Returns a function that registers new routes for an extension.
# The returned function encapsulates (creates a closure around)
# the `app` object but does expose it.
def register_new_ext_routes_fn(ext: Extension):
register_ext_routes(app, ext)
return register_new_ext_routes_fn
def register_new_ratelimiter(app: FastAPI) -> Callable:
def register_new_ratelimiter_fn():
limiter = Limiter(
key_func=get_remote_address,
default_limits=[
f"{settings.lnbits_rate_limit_no}/{settings.lnbits_rate_limit_unit}"
],
)
app.state.limiter = limiter
return register_new_ratelimiter_fn
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
"""Register FastAPI routes for extension."""
2022-11-29 17:14:53 +01:00
ext_module = importlib.import_module(ext.module_name)
2022-11-29 16:03:46 +01:00
ext_route = getattr(ext_module, f"{ext.code}_ext")
if hasattr(ext_module, f"{ext.code}_start"):
ext_start_func = getattr(ext_module, f"{ext.code}_start")
ext_start_func()
if hasattr(ext_module, f"{ext.code}_static_files"):
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
for s in ext_statics:
static_dir = Path(
settings.lnbits_extensions_path, "extensions", *s["path"].split("/")
)
app.mount(s["path"], StaticFiles(directory=static_dir), s["name"])
if hasattr(ext_module, f"{ext.code}_redirect_paths"):
ext_redirects = getattr(ext_module, f"{ext.code}_redirect_paths")
settings.lnbits_extensions_redirects = [
r for r in settings.lnbits_extensions_redirects if r["ext_id"] != ext.code
]
for r in ext_redirects:
r["ext_id"] = ext.code
settings.lnbits_extensions_redirects.append(r)
logger.trace(f"adding route for extension {ext_module}")
2022-11-29 17:14:53 +01:00
2023-01-25 13:32:41 +01:00
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash != "" else ""
2022-11-29 17:14:53 +01:00
app.include_router(router=ext_route, prefix=prefix)
def register_startup(app: FastAPI):
@app.on_event("startup")
async def lnbits_startup():
2022-12-09 09:31:29 +01:00
try:
2023-01-18 15:25:44 +01:00
# wait till migration is done
2022-12-09 09:31:29 +01:00
await migrate_databases()
2023-01-18 15:25:44 +01:00
# setup admin settings
2022-12-09 09:31:29 +01:00
await check_admin_settings()
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
await check_webpush_settings()
2022-12-09 09:31:29 +01:00
log_server_info()
2023-01-18 15:25:44 +01:00
# initialize WALLET
try:
set_wallet_class()
except Exception as e:
logger.error(
2023-10-26 15:44:12 +02:00
f"Error initializing {settings.lnbits_backend_wallet_class}: {e}"
)
set_void_wallet_class()
2023-01-18 15:25:44 +01:00
# initialize funding source
2022-12-09 09:31:29 +01:00
await check_funding_source()
2023-01-18 17:35:02 +01:00
# check extensions after restart
await check_installed_extensions(app)
# register core and extension routes
register_routes(app)
if settings.lnbits_admin_ui:
initialize_server_logger()
2022-12-09 09:31:29 +01:00
except Exception as e:
logger.error(str(e))
raise ImportError("Failed to run 'startup' event.")
def register_shutdown(app: FastAPI):
@app.on_event("shutdown")
async def on_shutdown():
cancel_all_tasks()
# wait a bit to allow them to finish, so that cleanup can run without problems
await asyncio.sleep(0.1)
WALLET = get_wallet_class()
await WALLET.cleanup()
def initialize_server_logger():
super_user_hash = sha256(settings.super_user.encode("utf-8")).hexdigest()
serverlog_queue = asyncio.Queue()
async def update_websocket_serverlog():
while True:
msg = await serverlog_queue.get()
await websocketUpdater(super_user_hash, msg)
create_permanent_task(update_websocket_serverlog)
logger.add(
lambda msg: serverlog_queue.put_nowait(msg),
format=Formatter().format,
)
2022-12-05 12:28:26 +01:00
def log_server_info():
logger.info("Starting LNbits")
logger.info(f"Version: {settings.version}")
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
2022-12-05 12:28:26 +01:00
logger.info(f"Host: {settings.host}")
logger.info(f"Port: {settings.port}")
logger.info(f"Debug: {settings.debug}")
logger.info(f"Site title: {settings.lnbits_site_title}")
logger.info(f"Funding source: {settings.lnbits_backend_wallet_class}")
logger.info(f"Data folder: {settings.lnbits_data_folder}")
logger.info(f"Database: {get_db_vendor_name()}")
logger.info(f"Service fee: {settings.lnbits_service_fee}")
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
2022-12-05 12:28:26 +01:00
def get_db_vendor_name():
db_url = settings.lnbits_database_url
return (
"PostgreSQL"
if db_url and db_url.startswith("postgres://")
else (
"CockroachDB"
if db_url and db_url.startswith("cockroachdb://")
else "SQLite"
)
2022-12-05 12:28:26 +01:00
)
2020-09-28 04:12:55 +02:00
def register_async_tasks(app):
@app.on_event("startup")
2020-09-28 04:12:55 +02:00
async def listeners():
create_permanent_task(check_pending_payments)
create_permanent_task(invoice_listener)
create_permanent_task(internal_invoice_listener)
create_permanent_task(cache.invalidate_forever)
# core invoice listener
invoice_queue = asyncio.Queue(5)
register_invoice_listener(invoice_queue, "core")
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
# TODO: implement watchdog properly
# create_permanent_task(watchdog_task)
create_permanent_task(killswitch_task)
2021-10-17 19:33:29 +02:00
def register_exception_handlers(app: FastAPI):
@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
etype, _, tb = sys.exc_info()
traceback.print_exception(etype, exc, tb)
2022-12-14 18:40:07 +01:00
logger.error(f"Exception: {str(exc)}")
2022-12-14 19:09:01 +01:00
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
2022-06-15 17:20:20 +02:00
return template_renderer().TemplateResponse(
2022-12-14 18:40:07 +01:00
"error.html", {"request": request, "err": f"Error: {str(exc)}"}
2022-06-15 17:20:20 +02:00
)
2022-06-15 17:49:39 +02:00
2022-06-15 17:20:20 +02:00
return JSONResponse(
2022-12-14 14:59:11 +01:00
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
2022-12-14 18:40:07 +01:00
content={"detail": str(exc)},
2021-10-17 19:33:29 +02:00
)
2022-12-14 14:33:13 +01:00
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
2022-12-14 18:40:07 +01:00
logger.error(f"RequestValidationError: {str(exc)}")
2022-12-14 14:33:13 +01:00
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
2022-06-15 17:37:10 +02:00
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
2022-06-15 17:20:20 +02:00
return template_renderer().TemplateResponse(
2022-12-14 14:33:13 +01:00
"error.html",
2022-12-14 18:40:07 +01:00
{"request": request, "err": f"Error: {str(exc)}"},
2022-06-15 17:20:20 +02:00
)
2022-06-15 17:49:39 +02:00
2022-06-15 17:20:20 +02:00
return JSONResponse(
2022-12-14 14:59:11 +01:00
status_code=HTTPStatus.BAD_REQUEST,
2022-12-14 18:40:07 +01:00
content={"detail": str(exc)},
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
# Only the browser sends "text/html" request
# not fail proof, but everything else get's a JSON response
if (
request.headers
and "accept" in request.headers
and "text/html" in request.headers["accept"]
):
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
if exc.headers and "token-expired" in exc.headers:
response = RedirectResponse("/")
response.delete_cookie("cookie_access_token")
response.delete_cookie("is_lnbits_user_authorized")
response.set_cookie("is_access_token_expired", "true")
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 11:38:19 +01:00
return response
2022-12-14 18:40:07 +01:00
return template_renderer().TemplateResponse(
"error.html",
{
"request": request,
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
},
)
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
2021-10-17 19:33:29 +02:00
)
2022-07-07 16:24:36 +02:00
def configure_logger() -> None:
logger.remove()
log_level: str = "DEBUG" if settings.debug else "INFO"
formatter = Formatter()
logger.add(sys.stdout, level=log_level, format=formatter.format)
if settings.enable_log_to_file:
logger.add(
Path(settings.lnbits_data_folder, "logs", "lnbits.log"),
rotation=settings.log_rotation,
retention=settings.log_retention,
level="INFO",
format=formatter.format,
)
logger.add(
Path(settings.lnbits_data_folder, "logs", "debug.log"),
rotation=settings.log_rotation,
retention=settings.log_retention,
level="DEBUG",
format=formatter.format,
)
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
logging.getLogger("uvicorn.error").propagate = False
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base").propagate = False
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
class Formatter:
def __init__(self):
self.padding = 0
self.minimal_fmt = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | "
"<level>{message}</level>\n"
)
if settings.debug:
self.fmt = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | "
"<level>{level: <4}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
"<level>{message}</level>\n"
2023-04-17 08:29:01 +02:00
)
else:
self.fmt = self.minimal_fmt
def format(self, record):
function = "{function}".format(**record)
if function == "emit": # uvicorn logs
return self.minimal_fmt
return self.fmt
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
logger.log(level, record.getMessage())