mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-24 14:51:05 +01:00
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>
189 lines
6.7 KiB
Python
189 lines
6.7 KiB
Python
import json
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Any, List, Optional, Type
|
|
|
|
import jinja2
|
|
import shortuuid
|
|
from jose import jwt
|
|
from pydantic import BaseModel
|
|
from pydantic.schema import field_schema
|
|
|
|
from lnbits.jinja2_templating import Jinja2Templates
|
|
from lnbits.nodes import get_node_class
|
|
from lnbits.requestvars import g
|
|
from lnbits.settings import settings
|
|
|
|
from .db import FilterModel
|
|
from .extension_manager import get_valid_extensions
|
|
|
|
|
|
def urlsafe_short_hash() -> str:
|
|
return shortuuid.uuid()
|
|
|
|
|
|
def url_for(endpoint: str, external: Optional[bool] = False, **params: Any) -> str:
|
|
base = g().base_url if external else ""
|
|
url_params = "?"
|
|
for key, value in params.items():
|
|
url_params += f"{key}={value}&"
|
|
url = f"{base}{endpoint}{url_params}"
|
|
return url
|
|
|
|
|
|
def static_url_for(static: str, path: str) -> str:
|
|
return f"/{static}/{path}?v={settings.server_startup_time}"
|
|
|
|
|
|
def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templates:
|
|
folders = ["lnbits/templates", "lnbits/core/templates"]
|
|
if additional_folders:
|
|
additional_folders += [
|
|
Path(settings.lnbits_extensions_path, "extensions", f)
|
|
for f in additional_folders
|
|
]
|
|
folders.extend(additional_folders)
|
|
t = Jinja2Templates(loader=jinja2.FileSystemLoader(folders))
|
|
t.env.globals["static_url_for"] = static_url_for
|
|
|
|
if settings.lnbits_ad_space_enabled:
|
|
t.env.globals["AD_SPACE"] = settings.lnbits_ad_space.split(",")
|
|
t.env.globals["AD_SPACE_TITLE"] = settings.lnbits_ad_space_title
|
|
|
|
t.env.globals["VOIDWALLET"] = settings.lnbits_backend_wallet_class == "VoidWallet"
|
|
t.env.globals["HIDE_API"] = settings.lnbits_hide_api
|
|
t.env.globals["SITE_TITLE"] = settings.lnbits_site_title
|
|
t.env.globals["LNBITS_DENOMINATION"] = settings.lnbits_denomination
|
|
t.env.globals["SITE_TAGLINE"] = settings.lnbits_site_tagline
|
|
t.env.globals["SITE_DESCRIPTION"] = settings.lnbits_site_description
|
|
t.env.globals["LNBITS_THEME_OPTIONS"] = settings.lnbits_theme_options
|
|
t.env.globals["LNBITS_QR_LOGO"] = settings.lnbits_qr_logo
|
|
t.env.globals["LNBITS_VERSION"] = settings.version
|
|
t.env.globals["LNBITS_NEW_ACCOUNTS_ALLOWED"] = settings.new_accounts_allowed
|
|
t.env.globals["LNBITS_AUTH_METHODS"] = settings.auth_allowed_methods
|
|
t.env.globals["LNBITS_ADMIN_UI"] = settings.lnbits_admin_ui
|
|
t.env.globals["LNBITS_SERVICE_FEE"] = settings.lnbits_service_fee
|
|
t.env.globals["LNBITS_SERVICE_FEE_MAX"] = settings.lnbits_service_fee_max
|
|
t.env.globals["LNBITS_SERVICE_FEE_WALLET"] = settings.lnbits_service_fee_wallet
|
|
t.env.globals["LNBITS_NODE_UI"] = (
|
|
settings.lnbits_node_ui and get_node_class() is not None
|
|
)
|
|
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
|
|
t.env.globals["EXTENSIONS"] = [
|
|
e
|
|
for e in get_valid_extensions()
|
|
if e.code not in settings.lnbits_deactivated_extensions
|
|
]
|
|
if settings.lnbits_custom_logo:
|
|
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo
|
|
|
|
if settings.bundle_assets:
|
|
t.env.globals["INCLUDED_JS"] = ["bundle.min.js"]
|
|
t.env.globals["INCLUDED_CSS"] = ["bundle.min.css"]
|
|
else:
|
|
vendor_filepath = Path(settings.lnbits_path, "static", "vendor.json")
|
|
with open(vendor_filepath) as vendor_file:
|
|
vendor_files = json.loads(vendor_file.read())
|
|
t.env.globals["INCLUDED_JS"] = vendor_files["js"]
|
|
t.env.globals["INCLUDED_CSS"] = vendor_files["css"]
|
|
|
|
t.env.globals["WEBPUSH_PUBKEY"] = settings.lnbits_webpush_pubkey
|
|
|
|
return t
|
|
|
|
|
|
def get_current_extension_name() -> str:
|
|
"""
|
|
Returns the name of the extension that calls this method.
|
|
"""
|
|
import inspect
|
|
import json
|
|
import os
|
|
|
|
callee_filepath = inspect.stack()[1].filename
|
|
callee_dirname, _ = os.path.split(callee_filepath)
|
|
|
|
path = os.path.normpath(callee_dirname)
|
|
extension_director_name = path.split(os.sep)[-1]
|
|
try:
|
|
config_path = os.path.join(callee_dirname, "config.json")
|
|
with open(config_path) as json_file:
|
|
config = json.load(json_file)
|
|
ext_name = config["name"]
|
|
except Exception:
|
|
ext_name = extension_director_name
|
|
return ext_name
|
|
|
|
|
|
def generate_filter_params_openapi(model: Type[FilterModel], keep_optional=False):
|
|
"""
|
|
Generate openapi documentation for Filters. This is intended to be used along
|
|
parse_filters (see example)
|
|
:param model: Filter model
|
|
:param keep_optional: If false, all parameters will be optional,
|
|
otherwise inferred from model
|
|
"""
|
|
fields = list(model.__fields__.values())
|
|
params = []
|
|
for field in fields:
|
|
schema, _, _ = field_schema(field, model_name_map={})
|
|
|
|
description = "Supports Filtering"
|
|
if (
|
|
hasattr(model, "__search_fields__")
|
|
and field.name in model.__search_fields__
|
|
):
|
|
description += ". Supports Search"
|
|
|
|
parameter = {
|
|
"name": field.alias,
|
|
"in": "query",
|
|
"required": field.required if keep_optional else False,
|
|
"schema": schema,
|
|
"description": description,
|
|
}
|
|
params.append(parameter)
|
|
|
|
return {
|
|
"parameters": params,
|
|
}
|
|
|
|
|
|
def insert_query(table_name: str, model: BaseModel) -> str:
|
|
"""
|
|
Generate an insert query with placeholders for a given table and model
|
|
:param table_name: Name of the table
|
|
:param model: Pydantic model
|
|
"""
|
|
placeholders = ", ".join(["?"] * len(model.dict().keys()))
|
|
fields = ", ".join(model.dict().keys())
|
|
return f"INSERT INTO {table_name} ({fields}) VALUES ({placeholders})"
|
|
|
|
|
|
def update_query(table_name: str, model: BaseModel, where: str = "WHERE id = ?") -> str:
|
|
"""
|
|
Generate an update query with placeholders for a given table and model
|
|
:param table_name: Name of the table
|
|
:param model: Pydantic model
|
|
:param where: Where string, default to `WHERE id = ?`
|
|
"""
|
|
query = ", ".join([f"{field} = ?" for field in model.dict().keys()])
|
|
return f"UPDATE {table_name} SET {query} {where}"
|
|
|
|
|
|
def is_valid_email_address(email: str) -> bool:
|
|
email_regex = r"[A-Za-z0-9\._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,63}"
|
|
return re.fullmatch(email_regex, email) is not None
|
|
|
|
|
|
def is_valid_username(username: str) -> bool:
|
|
username_regex = r"(?=[a-zA-Z0-9._]{2,20}$)(?!.*[_.]{2})[^_.].*[^_.]"
|
|
return re.fullmatch(username_regex, username) is not None
|
|
|
|
|
|
def create_access_token(data: dict):
|
|
expire = datetime.utcnow() + timedelta(minutes=settings.auth_token_expire_minutes)
|
|
to_encode = data.copy()
|
|
to_encode.update({"exp": expire})
|
|
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|