diff --git a/CUSTOM_FRONTEND_URL_SIMPLE.md b/CUSTOM_FRONTEND_URL_SIMPLE.md deleted file mode 100644 index 96c2d052..00000000 --- a/CUSTOM_FRONTEND_URL_SIMPLE.md +++ /dev/null @@ -1,213 +0,0 @@ -# Custom Frontend URL - Simple Redirect Approach - -## Overview - -This is the **simplest** approach to integrate LNbits with a custom frontend. LNbits handles all authentication (login, register, password reset) and then redirects users to your custom frontend URL instead of `/wallet`. - -## How It Works - -### Password Reset Flow - -1. **Admin generates reset link** → User receives: `https://lnbits.com/?reset_key=...` -2. **User clicks link** → LNbits shows password reset form -3. **User submits new password** → LNbits sets auth cookies -4. **LNbits redirects** → `https://myapp.com/` (your custom frontend) -5. **Web-app loads** → `checkAuth()` sees valid LNbits cookies → ✅ User is logged in! - -### Login Flow - -1. **User visits** → `https://lnbits.com/` -2. **User logs in** → LNbits validates credentials and sets cookies -3. **LNbits redirects** → `https://myapp.com/` -4. **Web-app loads** → ✅ User is logged in! - -### Register Flow - -1. **User visits** → `https://lnbits.com/` -2. **User registers** → LNbits creates account and sets cookies -3. **LNbits redirects** → `https://myapp.com/` -4. **Web-app loads** → ✅ User is logged in! - -## Configuration - -### Environment Variable - -```bash -# In .env or environment -export LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com -``` - -Or configure through LNbits admin UI: **Settings → Operations → Custom Frontend URL** - -### Default Behavior - -- **If not set**: Redirects to `/wallet` (default LNbits behavior) -- **If set**: Redirects to your custom frontend URL - -## Implementation - -### Changes Made - -1. **Added setting** (`lnbits/settings.py:282-285`): - ```python - class OpsSettings(LNbitsSettings): - lnbits_custom_frontend_url: str | None = Field( - default=None, - description="Custom frontend URL for post-auth redirects" - ) - ``` - -2. **Exposed to frontend** (`lnbits/helpers.py:88`): - ```python - window_settings = { - # ... - "LNBITS_CUSTOM_FRONTEND_URL": settings.lnbits_custom_frontend_url, - # ... - } - ``` - -3. **Updated redirects** (`lnbits/static/js/index.js`): - - `login()` - line 78 - - `register()` - line 56 - - `reset()` - line 68 - - `loginUsr()` - line 88 - - All now use: - ```javascript - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL || '/wallet' - ``` - -## Advantages - -### ✅ Zero Changes to Web-App -Your custom frontend doesn't need to: -- Parse `?reset_key=` from URLs -- Build password reset UI -- Handle password reset API calls -- Manage error states -- Implement validation - -### ✅ Auth Cookies Work Automatically -LNbits sets httponly cookies that your web-app automatically sees: -- `cookie_access_token` -- `is_lnbits_user_authorized` - -Your existing `auth.checkAuth()` will detect these and log the user in. - -### ✅ Simple & Elegant -Only 3 files changed in LNbits, zero changes in web-app. - -### ✅ Backwards Compatible -Existing LNbits installations continue to work. Setting is optional. - -## Disadvantages - -### ⚠️ Brief Branding Inconsistency -Users see LNbits UI briefly during: -- Login form -- Registration form -- Password reset form - -Then get redirected to your branded web-app. - -**This is usually acceptable** for most use cases, especially for password reset which is infrequent. - -## Testing - -1. **Set the environment variable**: - ```bash - export LNBITS_CUSTOM_FRONTEND_URL=http://localhost:5173 - ``` - -2. **Restart LNbits**: - ```bash - poetry run lnbits - ``` - -3. **Test Login**: - - Visit `http://localhost:5000/` - - Log in with credentials - - Verify redirect to `http://localhost:5173` - - Verify web-app shows you as logged in - -4. **Test Password Reset**: - - Admin generates reset link in users panel - - User clicks link with `?reset_key=...` - - User enters new password - - Verify redirect to custom frontend - - Verify web-app shows you as logged in - -## Security - -### ✅ Secure -- Auth cookies are httponly (can't be accessed by JavaScript) -- LNbits handles all auth logic -- No sensitive data in URLs except one-time reset keys -- Reset keys expire based on `auth_token_expire_minutes` - -### 🔒 HTTPS Required -Always use HTTPS for custom frontend URLs in production: -```bash -export LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com -``` - -## Migration - -**No database migration required!** - -Settings are stored as JSON in `system_settings` table. New fields are automatically included. - -## Alternative: Full Custom Frontend Approach - -If you need **complete branding consistency** (no LNbits UI shown), you would need to: - -1. Build password reset form in web-app -2. Parse `?reset_key=` from URL -3. Add API method to call `/api/v1/auth/reset` -4. Handle validation, errors, loading states -5. Update admin UI to generate links pointing to web-app - -This is **significantly more work** for marginal benefit (users see LNbits UI for ~5 seconds during password reset). - -## Recommendation - -**Use this simple approach** unless you have specific requirements for complete UI consistency. The brief LNbits UI is a small trade-off for the simplicity gained. - -## Related Files - -- `lnbits/settings.py` - Setting definition -- `lnbits/helpers.py` - Expose to frontend -- `lnbits/static/js/index.js` - Redirect logic - -## Example `.env` - -```bash -# LNbits Configuration -LNBITS_DATA_FOLDER=./data -LNBITS_DATABASE_URL=sqlite:///./data/database.sqlite3 - -# Custom Frontend Integration -LNBITS_CUSTOM_FRONTEND_URL=https://myapp.com - -# Other settings... -``` - -## How Web-App Benefits - -Your web-app at `https://myapp.com` can now: - -1. **Receive logged-in users** from LNbits without any code changes -2. **Use existing `auth.checkAuth()`** - it just works -3. **Focus on your features** - don't rebuild auth UI -4. **Trust LNbits security** - it's battle-tested - -The auth cookies LNbits sets are valid for your domain if LNbits is on a subdomain (e.g., `api.myapp.com`) or you're using proper CORS configuration. - -## Future Enhancement - -If you later need the full custom UI approach, all the groundwork is there: -- Setting exists and is configurable -- Just add the web-app UI components -- Update admin panel to generate web-app links - -But start with this simple approach first! 🚀 diff --git a/extensions.json b/extensions.json deleted file mode 100644 index aca0811d..00000000 --- a/extensions.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "featured": [ - "satmachineclient", - "castle" - ], - "extensions": [ - { - "id": "satmachineadmin", - "repo": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin", - "name": "Satoshi Machine Admin", - "version": "0.0.2", - "short_description": "Admin Dashboard for Satoshi Machine", - "icon": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/satmachineadmin/archive/v0.0.2.zip", - "hash": "e46863d3c1133897d9fe9cc4afe149804df25b2aeee1716de4da4332ae9789b1" - }, - { - "id": "satmachineclient", - "repo": "https://git.aiolabs.dev/lnbits-exts/satmachineclient", - "name": "Satoshi Machine Client", - "version": "0.0.1", - "short_description": "Client Dashboard for Satoshi Machine", - "icon": "https://git.aiolabs.dev/lnbits-exts/satmachineclient/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/satmachineclient/archive/v0.0.1.zip", - "hash": "6d93e1a537ca3565fcf4dc4811497de10660a14639046854c1006b06d9a045e5" - }, - { - "id": "events", - "repo": "https://git.aiolabs.dev/lnbits-exts/events", - "name": "AIO Events", - "version": "0.0.1", - "short_description": "AIO fork of lnbits-exts/events", - "icon": "https://git.aiolabs.dev/lnbits-exts/events/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/events/archive/v0.0.1.zip", - "hash": "681408f3f5f5b0773f0aa5fd7fbdefcd47ed9190b44409b80d2b119cc5516335" - }, - { - "id": "nostrrelay", - "repo": "https://git.aiolabs.dev/lnbits-exts/nostrrelay", - "name": "AIO Nostrrelay", - "version": "0.0.2", - "short_description": "AIO fork of lnbits-exts/nostrrelay", - "icon": "https://git.aiolabs.dev/lnbits-exts/nostrrelay/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/nostrrelay/archive/v0.0.2.zip", - "hash": "a46475f076557f5c5a426a9e3cbb680af855b833a9f66ba32b6f36778336302f" - }, - { - "id": "nostrclient", - "repo": "https://git.aiolabs.dev/lnbits-exts/nostrclient", - "name": "AIO nostrclient", - "version": "0.0.1", - "short_description": "AIO fork of lnbits-exts/nostrclient", - "icon": "https://git.aiolabs.dev/lnbits-exts/nostrclient/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/nostrclient/archive/v0.0.1.zip", - "hash": "c71c6bd2105e299a2ff342fb9d94d619570f28f231a3025f52f29757f9b31d04" - }, - { - "id": "nostrmarket", - "repo": "https://git.aiolabs.dev/lnbits-exts/nostrmarket", - "name": "AIO nostrmarket", - "version": "0.0.1", - "short_description": "AIO fork of lnbits-exts/nostrmarket", - "icon": "https://git.aiolabs.dev/lnbits-exts/nostrmarket/raw/branch/main/static/image/aio.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/nostrmarket/archive/v0.0.1.zip", - "hash": "5177dfdae62554c75ba8244d5a26e371de6e512a8b6dc7bb6f90e8b05916ed35" - }, - { - "id": "castle", - "repo": "https://git.aiolabs.dev/lnbits-exts/castle", - "name": "Castle", - "version": "0.0.1", - "short_description": "Castle Accounting", - "icon": "https://git.aiolabs.dev/lnbits-exts/castle/raw/branch/main/static/image/castle.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/castle/archive/v0.0.1.zip", - "hash": "0b152190902e7377bfba9feb4c82998a9400a69413ee5482343f15da8308b89b" - }, - { - "id": "castle", - "repo": "https://git.aiolabs.dev/lnbits-exts/castle", - "name": "Castle", - "version": "0.0.2", - "short_description": "Castle Accounting", - "icon": "https://git.aiolabs.dev/lnbits-exts/castle/raw/branch/main/static/image/castle.png", - "archive": "https://git.aiolabs.dev/lnbits-exts/castle/archive/v0.0.2.zip", - "hash": "0f742deb4777480a32ad239f6eae42c57583bf7a8ca9bd131903b6bb7282f069" - }, - { - "id": "lnurlp", - "repo": "https://github.com/lnbits/lnurlp", - "name": "Pay Links", - "version": "1.1.3", - "min_lnbits_version": "1.3.0", - "short_description": "Make reusable LNURL pay links", - "icon": "https://github.com/lnbits/lnurlp/raw/main/static/image/lnurl-pay.png", - "details_link": "https://raw.githubusercontent.com/lnbits/lnurlp/main/config.json", - "archive": "https://github.com/lnbits/lnurlp/archive/refs/tags/v1.1.3.zip", - "hash": "913b6880fd824e801f05ae50ed6f57817c9a580f1ed1b02b435823d5754e1b98", - "max_lnbits_version": "1.4.0" - }, - { - "id": "lnurlp", - "repo": "https://github.com/lnbits/lnurlp", - "name": "Pay Links", - "version": "1.2.0", - "min_lnbits_version": "1.4.0", - "short_description": "Make reusable LNURL pay links", - "icon": "https://github.com/lnbits/lnurlp/raw/main/static/image/lnurl-pay.png", - "details_link": "https://raw.githubusercontent.com/lnbits/lnurlp/main/config.json", - "archive": "https://github.com/lnbits/lnurlp/archive/refs/tags/v1.2.0.zip", - "hash": "d3872eb5f65b9e962fc201d6784f94f3fd576284582b980805142a2b3f62d8e8" - } - ] -} diff --git a/lnbits/core/services/users.py b/lnbits/core/services/users.py index d21a8aff..a3c9b800 100644 --- a/lnbits/core/services/users.py +++ b/lnbits/core/services/users.py @@ -1,5 +1,3 @@ -import json -import time from pathlib import Path from uuid import uuid4 @@ -74,7 +72,7 @@ async def create_user_account_no_ckeck( account.id = uuid4().hex account = await create_account(account, conn=conn) - wallet = await create_wallet( + await create_wallet( user_id=account.id, wallet_name=wallet_name or settings.lnbits_default_wallet_name, conn=conn, @@ -88,22 +86,6 @@ async def create_user_account_no_ckeck( except Exception as e: logger.error(f"Error enabeling default extension {ext_id}: {e}") - # Create default pay link for users with username - if account.username and "lnurlp" in user_extensions: - try: - await _create_default_pay_link(account, wallet) - logger.info(f"Created default pay link for user {account.username}") - except Exception as e: - logger.error(f"Failed to create default pay link for user {account.username}: {e}") - - # Publish Nostr kind 0 metadata event if user has username and Nostr keys - if account.username and account.pubkey and account.prvkey: - try: - await _publish_nostr_metadata_event(account) - logger.info(f"Published Nostr metadata event for user {account.username}") - except Exception as e: - logger.error(f"Failed to publish Nostr metadata for user {account.username}: {e}") - user = await get_user_from_account(account, conn=conn) if not user: raise ValueError("Cannot find user for account.") @@ -210,160 +192,3 @@ async def init_admin_settings(super_user: str | None = None) -> SuperSettings: editable_settings = EditableSettings.from_dict(settings.dict()) return await create_admin_settings(account.id, editable_settings.dict()) - - -async def _create_default_pay_link(account: Account, wallet) -> None: - """Create a default pay link for new users with username (Bitcoinmat receiving address)""" - try: - # Try dynamic import that works with extensions in different locations - import importlib - import sys - - # First try the standard import path - try: - lnurlp_crud = importlib.import_module("lnbits.extensions.lnurlp.crud") - lnurlp_models = importlib.import_module("lnbits.extensions.lnurlp.models") - except ImportError: - # If that fails, try importing from external extensions path - # This handles cases where extensions are in /var/lib/lnbits/extensions - try: - # Add extensions path to sys.path if not already there - extensions_path = ( - settings.lnbits_extensions_path or "/var/lib/lnbits/extensions" - ) - if extensions_path not in sys.path: - sys.path.insert(0, extensions_path) - - lnurlp_crud = importlib.import_module("lnurlp.crud") - lnurlp_models = importlib.import_module("lnurlp.models") - except ImportError as e: - logger.warning(f"lnurlp extension not found in any location: {e}") - return - - create_pay_link = lnurlp_crud.create_pay_link - CreatePayLinkData = lnurlp_models.CreatePayLinkData - - pay_link_data = CreatePayLinkData( - description="Bitcoinmat Receiving Address", - wallet=wallet.id, - # Note default `currency` is satoshis when set as NULL in db - min=1, # minimum 1 sat - max=500000, # maximum 500,000 sats - comment_chars=140, - username=account.username, # use the username as lightning address - zaps=True, - disposable=True, - ) - - await create_pay_link(pay_link_data) - - logger.info( - f"Successfully created default pay link for user {account.username}" - ) - - except Exception as e: - logger.error(f"Failed to create default pay link: {e}") - # Don't raise - we don't want user creation to fail if pay link creation fails - - -async def _publish_nostr_metadata_event(account: Account) -> None: - """Publish a Nostr kind 0 metadata event for a new user""" - import coincurve - - from lnbits.utils.nostr import sign_event - - # Create Nostr kind 0 metadata event - metadata = { - "name": account.username, - "display_name": account.username, - "about": f"LNbits user: {account.username}", - } - - event = { - "kind": 0, - "created_at": int(time.time()), - "tags": [], - "content": json.dumps(metadata), - } - - # Convert hex private key to coincurve PrivateKey - private_key = coincurve.PrivateKey(bytes.fromhex(account.prvkey)) - - # Sign the event - signed_event = sign_event(event, account.pubkey, private_key) - - # Publish directly to nostrrelay database (hacky but works around WebSocket issues) - await _insert_event_into_nostrrelay(signed_event, account.username) - - -async def _insert_event_into_nostrrelay(event: dict, username: str) -> None: - """Directly insert Nostr event into nostrrelay database (hacky workaround)""" - try: - import importlib - import sys - - # Try to import nostrrelay from various possible locations - nostrrelay_crud = None - NostrEvent = None - try: - nostrrelay_crud = importlib.import_module( - "lnbits.extensions.nostrrelay.crud" - ) - NostrEvent = importlib.import_module( - "lnbits.extensions.nostrrelay.relay.event" - ).NostrEvent - except ImportError: - try: - # Check if nostrrelay is in external extensions path - extensions_path = ( - settings.lnbits_extensions_path or "/var/lib/lnbits/extensions" - ) - if extensions_path not in sys.path: - sys.path.insert(0, extensions_path) - nostrrelay_crud = importlib.import_module("nostrrelay.crud") - NostrEvent = importlib.import_module( - "nostrrelay.relay.event" - ).NostrEvent - except ImportError: - # Try from the lnbits-nostrmarket project path - nostrmarket_path = "/home/padreug/Projects/lnbits-nostrmarket" - if nostrmarket_path not in sys.path: - sys.path.insert(0, nostrmarket_path) - nostrrelay_crud = importlib.import_module("nostrrelay.crud") - NostrEvent = importlib.import_module( - "nostrrelay.relay.event" - ).NostrEvent - - if not nostrrelay_crud or not NostrEvent: - logger.warning( - "Could not import nostrrelay - skipping direct database insert" - ) - return - - # Use a default relay_id for the proof of concept - relay_id = "test1" - logger.debug(f"Using relay_id: {relay_id} for nostrrelay event") - - # Create NostrEvent object for nostrrelay - nostr_event = NostrEvent( - id=event["id"], - relay_id=relay_id, - publisher=event["pubkey"], - pubkey=event["pubkey"], - created_at=event["created_at"], - kind=event["kind"], - tags=event.get("tags", []), - content=event["content"], - sig=event["sig"], - ) - - # Insert directly into nostrrelay database - await nostrrelay_crud.create_event(nostr_event) - - logger.info( - f"Successfully inserted Nostr metadata event for {username} into nostrrelay database" - ) - - except Exception as e: - logger.error(f"Failed to insert event into nostrrelay database: {e}") - logger.debug(f"Exception details: {type(e).__name__}: {str(e)}") diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 5d31423c..123776d7 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -100,16 +100,6 @@ async def api_list_currencies_available() -> list[str]: return allowed_currencies() -@api_router.get("/api/v1/default-currency") -async def api_get_default_currency() -> dict[str, str | None]: - """ - Get the default accounting currency for this LNbits instance. - Returns the configured default, or None if not set. - """ - default_currency = settings.lnbits_default_accounting_currency - return {"default_currency": default_currency} - - @api_router.post("/api/v1/conversion") async def api_fiat_as_sats(data: ConversionData): output = {} diff --git a/lnbits/core/views/user_api.py b/lnbits/core/views/user_api.py index fc0fd2d1..1b0301e8 100644 --- a/lnbits/core/views/user_api.py +++ b/lnbits/core/views/user_api.py @@ -96,9 +96,6 @@ async def api_create_user(data: CreateUser) -> CreateUser: data.extra = data.extra or UserExtra() data.extra.provider = data.extra.provider or "lnbits" - if data.pubkey: - data.pubkey = normalize_public_key(data.pubkey) - account = Account( id=uuid4().hex, username=data.username, diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 45d0424f..8780f0cc 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -87,7 +87,6 @@ def template_renderer(additional_folders: list | None = None) -> Jinja2Templates "LNBITS_CUSTOM_IMAGE": settings.lnbits_custom_image, "LNBITS_CUSTOM_BADGE": settings.lnbits_custom_badge, "LNBITS_CUSTOM_BADGE_COLOR": settings.lnbits_custom_badge_color, - "LNBITS_CUSTOM_FRONTEND_URL": settings.lnbits_custom_frontend_url, "LNBITS_EXTENSIONS_DEACTIVATE_ALL": settings.lnbits_extensions_deactivate_all, "LNBITS_NEW_ACCOUNTS_ALLOWED": settings.new_accounts_allowed, "LNBITS_NODE_UI": settings.lnbits_node_ui and settings.has_nodemanager, diff --git a/lnbits/settings.py b/lnbits/settings.py index 4c4ba6f9..2f469223 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -960,10 +960,6 @@ class EnvSettings(LNbitsSettings): lnbits_title: str = Field(default="LNbits API") lnbits_path: str = Field(default=".") lnbits_extensions_path: str = Field(default="lnbits") - lnbits_custom_frontend_url: str | None = Field( - default=None, - description="Custom frontend URL for redirects after auth (e.g., https://myapp.com). If not set, redirects to /wallet. This is read-only and must be set via environment variable." - ) super_user: str = Field(default="") auth_secret_key: str = Field(default="") version: str = Field(default="0.0.0") diff --git a/lnbits/static/js/pages/home.js b/lnbits/static/js/pages/home.js index 26d3105e..d3958d64 100644 --- a/lnbits/static/js/pages/home.js +++ b/lnbits/static/js/pages/home.js @@ -79,12 +79,7 @@ window.PageHome = { this.password, this.passwordRepeat ) - // Redirect to custom frontend URL if configured, otherwise use router - if (this.LNBITS_CUSTOM_FRONTEND_URL) { - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL - } else { - this.refreshAuthUser() - } + this.refreshAuthUser() } catch (e) { LNbits.utils.notifyApiError(e) } @@ -96,12 +91,7 @@ window.PageHome = { this.password, this.passwordRepeat ) - // Redirect to custom frontend URL if configured, otherwise use router - if (this.LNBITS_CUSTOM_FRONTEND_URL) { - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL - } else { - this.refreshAuthUser() - } + this.refreshAuthUser() } catch (e) { LNbits.utils.notifyApiError(e) } @@ -109,12 +99,7 @@ window.PageHome = { async login() { try { await LNbits.api.login(this.username, this.password) - // Redirect to custom frontend URL if configured, otherwise use router - if (this.LNBITS_CUSTOM_FRONTEND_URL) { - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL - } else { - this.refreshAuthUser() - } + this.refreshAuthUser() } catch (e) { LNbits.utils.notifyApiError(e) } @@ -122,13 +107,7 @@ window.PageHome = { async loginUsr() { try { await LNbits.api.loginUsr(this.usr) - this.usr = '' - // Redirect to custom frontend URL if configured, otherwise use router - if (this.LNBITS_CUSTOM_FRONTEND_URL) { - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL - } else { - this.refreshAuthUser() - } + this.refreshAuthUser() } catch (e) { console.warn(e) LNbits.utils.notifyApiError(e) @@ -159,28 +138,14 @@ window.PageHome = { } }, created() { + if (this.g.isUserAuthorized) { + return this.refreshAuthUser() + } const urlParams = new URLSearchParams(window.location.search) - - // Check for reset_key FIRST - password reset should work even if user is logged in this.reset_key = urlParams.get('reset_key') if (this.reset_key) { this.authAction = 'reset' - // Clear existing auth cookies to allow password reset to work - this.$q.cookies.remove('is_lnbits_user_authorized') - this.$q.cookies.remove('cookie_access_token') - return // Don't redirect to /wallet } - - // Redirect authorized users - if (this.g.isUserAuthorized) { - // Redirect to custom frontend URL if configured, otherwise use router - if (this.LNBITS_CUSTOM_FRONTEND_URL) { - window.location.href = this.LNBITS_CUSTOM_FRONTEND_URL - } else { - return this.refreshAuthUser() - } - } - // check if lightning parameters are present in the URL if (urlParams.has('lightning')) { this.lnurl = urlParams.get('lightning') diff --git a/misc-aio/AUTO_CREDIT_CHANGES.md b/misc-aio/AUTO_CREDIT_CHANGES.md deleted file mode 100644 index 408f6987..00000000 --- a/misc-aio/AUTO_CREDIT_CHANGES.md +++ /dev/null @@ -1,88 +0,0 @@ -# LNBits Auto-Credit Changes - -## Overview -Modified LNBits server to automatically credit new accounts with 1 million satoshis (1,000,000 sats) when they are created. - -## Changes Made - -### 1. Modified `lnbits/core/services/users.py` - -**Added imports:** -- `get_wallet` from `..crud` -- `update_wallet_balance` from `.payments` - -**Modified `create_user_account_no_ckeck` function:** -- Changed `create_wallet` call to capture the returned wallet object -- Added automatic credit of 1,000,000 sats after wallet creation -- Added error handling and logging for the credit operation - -**Code changes:** -```python -# Before: -await create_wallet( - user_id=account.id, - wallet_name=wallet_name or settings.lnbits_default_wallet_name, -) - -# After: -wallet = await create_wallet( - user_id=account.id, - wallet_name=wallet_name or settings.lnbits_default_wallet_name, -) - -# Credit new account with 1 million satoshis -try: - await update_wallet_balance(wallet, 1_000_000) - logger.info(f"Credited new account {account.id} with 1,000,000 sats") -except Exception as e: - logger.error(f"Failed to credit new account {account.id} with 1,000,000 sats: {e}") -``` - -### 2. Updated Tests in `tests/api/test_auth.py` - -**Modified test functions:** -- `test_register_ok`: Added balance verification for regular user registration -- `test_register_nostr_ok`: Added balance verification for Nostr authentication - -**Added assertions:** -```python -# Check that the wallet has 1 million satoshis -wallet = user.wallets[0] -assert wallet.balance == 1_000_000, f"Expected 1,000,000 sats balance, got {wallet.balance} sats" -``` - -## Affected Account Creation Paths - -The automatic credit will be applied to all new accounts created through: - -1. **Regular user registration** (`/api/v1/auth/register`) -2. **Nostr authentication** (`/api/v1/auth/nostr`) -3. **SSO login** (when new account is created) -4. **API account creation** (`/api/v1/account`) -5. **Admin user creation** (via admin interface) - -## Excluded Paths - -- **Superuser/Admin account creation** (`init_admin_settings`): This function creates the admin account directly and bypasses the user creation flow, so it won't receive the automatic credit. - -## Testing - -To test the changes: - -1. Install dependencies: `poetry install` -2. Run the modified tests: `poetry run pytest tests/api/test_auth.py::test_register_ok -v` -3. Run Nostr test: `poetry run pytest tests/api/test_auth.py::test_register_nostr_ok -v` - -## Logging - -The system will log: -- Success: `"Credited new account {account.id} with 1,000,000 sats"` -- Failure: `"Failed to credit new account {account.id} with 1,000,000 sats: {error}"` - -## Notes - -- The credit uses the existing `update_wallet_balance` function which creates an internal payment record -- The credit is applied after wallet creation but before user extensions are set up -- Error handling ensures that account creation continues even if the credit fails -- The credit amount is hardcoded to 1,000,000 sats (1MM sats) - diff --git a/misc-aio/lnbits-websocket-guide.md b/misc-aio/lnbits-websocket-guide.md deleted file mode 100644 index d6662b1d..00000000 --- a/misc-aio/lnbits-websocket-guide.md +++ /dev/null @@ -1,460 +0,0 @@ -# LNbits WebSocket Implementation Guide - -## Overview - -LNbits provides real-time WebSocket connections for monitoring wallet status, payment confirmations, and transaction updates. This guide covers how to implement and use these WebSocket connections in your applications. - -## WebSocket Endpoints - -### 1. Payment Monitoring WebSocket -- **URL**: `ws://localhost:5006/api/v1/ws/{wallet_inkey}` -- **HTTPS**: `wss://your-domain.com/api/v1/ws/{wallet_inkey}` -- **Purpose**: Real-time payment notifications and wallet updates - -### 2. Generic WebSocket Communication -- **URL**: `ws://localhost:5006/api/v1/ws/{item_id}` -- **Purpose**: Custom real-time communication channels - -## Client-Side Implementation - -### JavaScript/Browser Implementation - -#### Basic WebSocket Connection -```javascript -// Construct WebSocket URL -const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' -const websocketUrl = `${protocol}//${window.location.host}/api/v1/ws` - -// Connect to payment monitoring -const ws = new WebSocket(`${websocketUrl}/${wallet.inkey}`) - -// Handle incoming messages -ws.onmessage = (event) => { - const data = JSON.parse(event.data) - console.log('Received:', data) - - if (data.payment) { - handlePaymentReceived(data.payment) - } -} - -// Handle connection events -ws.onopen = () => { - console.log('WebSocket connected') -} - -ws.onclose = () => { - console.log('WebSocket disconnected') -} - -ws.onerror = (error) => { - console.error('WebSocket error:', error) -} -``` - -#### Using LNbits Built-in Event System -```javascript -// Using the built-in LNbits event system -LNbits.events.onInvoicePaid(wallet, (data) => { - if (data.payment) { - console.log('Payment confirmed:', data.payment) - - // Update UI - updateWalletBalance(data.payment.amount) - showPaymentNotification(data.payment) - } -}) -``` - -#### Vue.js Implementation Example -```javascript -// Vue component method -initWebSocket() { - const protocol = location.protocol === 'http:' ? 'ws://' : 'wss://' - const wsUrl = `${protocol}${document.domain}:${location.port}/api/v1/ws/${this.wallet.inkey}` - - this.ws = new WebSocket(wsUrl) - - this.ws.addEventListener('message', async ({ data }) => { - const response = JSON.parse(data.toString()) - - if (response.payment) { - // Handle payment update - await this.handlePaymentUpdate(response.payment) - } - }) - - this.ws.addEventListener('open', () => { - this.connectionStatus = 'connected' - }) - - this.ws.addEventListener('close', () => { - this.connectionStatus = 'disconnected' - // Implement reconnection logic - setTimeout(() => this.initWebSocket(), 5000) - }) -} -``` - -### Python Client Implementation - -```python -import asyncio -import websockets -import json - -async def listen_to_wallet(wallet_inkey, base_url="ws://localhost:5006"): - uri = f"{base_url}/api/v1/ws/{wallet_inkey}" - - try: - async with websockets.connect(uri) as websocket: - print(f"Connected to WebSocket: {uri}") - - async for message in websocket: - data = json.loads(message) - - if 'payment' in data: - payment = data['payment'] - print(f"Payment received: {payment['amount']} sat") - print(f"Payment hash: {payment['payment_hash']}") - - # Process payment - await handle_payment_received(payment) - - except websockets.exceptions.ConnectionClosed: - print("WebSocket connection closed") - except Exception as e: - print(f"WebSocket error: {e}") - -async def handle_payment_received(payment): - """Process incoming payment""" - # Update database - # Send notifications - # Update application state - pass - -# Run the WebSocket listener -if __name__ == "__main__": - wallet_inkey = "your_wallet_inkey_here" - asyncio.run(listen_to_wallet(wallet_inkey)) -``` - -### Node.js Client Implementation - -```javascript -const WebSocket = require('ws') - -class LNbitsWebSocketClient { - constructor(walletInkey, baseUrl = 'ws://localhost:5006') { - this.walletInkey = walletInkey - this.baseUrl = baseUrl - this.ws = null - this.reconnectInterval = 5000 - } - - connect() { - const url = `${this.baseUrl}/api/v1/ws/${this.walletInkey}` - this.ws = new WebSocket(url) - - this.ws.on('open', () => { - console.log(`Connected to LNbits WebSocket: ${url}`) - }) - - this.ws.on('message', (data) => { - try { - const message = JSON.parse(data.toString()) - this.handleMessage(message) - } catch (error) { - console.error('Error parsing WebSocket message:', error) - } - }) - - this.ws.on('close', () => { - console.log('WebSocket connection closed. Reconnecting...') - setTimeout(() => this.connect(), this.reconnectInterval) - }) - - this.ws.on('error', (error) => { - console.error('WebSocket error:', error) - }) - } - - handleMessage(message) { - if (message.payment) { - console.log('Payment received:', message.payment) - this.onPaymentReceived(message.payment) - } - } - - onPaymentReceived(payment) { - // Override this method to handle payments - console.log(`Received ${payment.amount} sat`) - } - - disconnect() { - if (this.ws) { - this.ws.close() - } - } -} - -// Usage -const client = new LNbitsWebSocketClient('your_wallet_inkey_here') -client.onPaymentReceived = (payment) => { - // Custom payment handling - console.log(`Processing payment: ${payment.payment_hash}`) -} -client.connect() -``` - -## Server-Side Implementation (LNbits Extensions) - -### Sending WebSocket Updates - -```python -from lnbits.core.services import websocket_manager - -async def notify_wallet_update(wallet_inkey: str, payment_data: dict): - """Send payment update to connected WebSocket clients""" - message = { - "payment": payment_data, - "timestamp": int(time.time()) - } - - await websocket_manager.send(wallet_inkey, json.dumps(message)) - -# Example usage in payment processing -async def process_payment_confirmation(payment_hash: str): - payment = await get_payment(payment_hash) - - if payment.wallet: - await notify_wallet_update(payment.wallet, { - "payment_hash": payment.payment_hash, - "amount": payment.amount, - "memo": payment.memo, - "status": "confirmed" - }) -``` - -### HTTP Endpoints for WebSocket Updates - -```python -# Send data via GET request -@router.get("/notify/{wallet_inkey}/{message}") -async def notify_wallet_get(wallet_inkey: str, message: str): - await websocket_manager.send(wallet_inkey, message) - return {"sent": True, "message": message} - -# Send data via POST request -@router.post("/notify/{wallet_inkey}") -async def notify_wallet_post(wallet_inkey: str, data: str): - await websocket_manager.send(wallet_inkey, data) - return {"sent": True, "data": data} -``` - -## Message Format - -### Payment Notification Message -```json -{ - "payment": { - "payment_hash": "abc123...", - "amount": 1000, - "memo": "Test payment", - "status": "confirmed", - "timestamp": 1640995200, - "fee": 1, - "wallet_id": "wallet_uuid" - } -} -``` - -### Custom Message Format -```json -{ - "type": "balance_update", - "wallet_id": "wallet_uuid", - "balance": 50000, - "timestamp": 1640995200 -} -``` - -## Best Practices - -### 1. Connection Management -- Implement automatic reconnection logic -- Handle connection timeouts gracefully -- Use exponential backoff for reconnection attempts - -### 2. Error Handling -```javascript -class WebSocketManager { - constructor(walletInkey) { - this.walletInkey = walletInkey - this.maxReconnectAttempts = 10 - this.reconnectAttempts = 0 - this.reconnectDelay = 1000 - } - - connect() { - try { - this.ws = new WebSocket(this.getWebSocketUrl()) - this.setupEventHandlers() - } catch (error) { - this.handleConnectionError(error) - } - } - - handleConnectionError(error) { - console.error('WebSocket connection error:', error) - - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++ - const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1) - - setTimeout(() => { - console.log(`Reconnection attempt ${this.reconnectAttempts}`) - this.connect() - }, delay) - } else { - console.error('Max reconnection attempts reached') - } - } -} -``` - -### 3. Message Validation -```javascript -function validatePaymentMessage(data) { - if (!data.payment) return false - - const payment = data.payment - return ( - typeof payment.payment_hash === 'string' && - typeof payment.amount === 'number' && - payment.amount > 0 && - ['pending', 'confirmed', 'failed'].includes(payment.status) - ) -} -``` - -### 4. Security Considerations -- Use HTTPS/WSS in production -- Validate wallet permissions before connecting -- Implement rate limiting for WebSocket connections -- Never expose admin keys through WebSocket messages - -## Testing WebSocket Connections - -### Using wscat (Command Line Tool) -```bash -# Install wscat -npm install -g wscat - -# Connect to WebSocket -wscat -c ws://localhost:5006/api/v1/ws/your_wallet_inkey - -# Test with SSL -wscat -c wss://your-domain.com/api/v1/ws/your_wallet_inkey -``` - -### Browser Console Testing -```javascript -// Open browser console and run: -const ws = new WebSocket('ws://localhost:5006/api/v1/ws/your_wallet_inkey') -ws.onmessage = (e) => console.log('Received:', JSON.parse(e.data)) -ws.onopen = () => console.log('Connected') -ws.onclose = () => console.log('Disconnected') -``` - -### Sending Test Messages -```bash -# Using curl to trigger WebSocket message -curl "http://localhost:5006/api/v1/ws/your_wallet_inkey/test_message" - -# Using POST -curl -X POST "http://localhost:5006/api/v1/ws/your_wallet_inkey" \ - -H "Content-Type: application/json" \ - -d '"test message data"' -``` - -## Troubleshooting - -### Common Issues - -1. **Connection Refused** - - Verify LNbits server is running on correct port - - Check firewall settings - - Ensure WebSocket endpoint is enabled - -2. **Authentication Errors** - - Verify wallet inkey is correct - - Check wallet permissions - - Ensure wallet exists and is active - -3. **Message Not Received** - - Check WebSocket connection status - - Verify message format - - Test with browser dev tools - -4. **Frequent Disconnections** - - Implement proper reconnection logic - - Check network stability - - Monitor server logs for errors - -### Debug Logging -```javascript -// Enable verbose WebSocket logging -const ws = new WebSocket(wsUrl) -ws.addEventListener('open', (event) => { - console.log('WebSocket opened:', event) -}) -ws.addEventListener('close', (event) => { - console.log('WebSocket closed:', event.code, event.reason) -}) -ws.addEventListener('error', (event) => { - console.error('WebSocket error:', event) -}) -``` - -## Production Deployment - -### Nginx Configuration -```nginx -location /api/v1/ws/ { - proxy_pass http://localhost:5006; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 86400; -} -``` - -### SSL/TLS Configuration -```nginx -server { - listen 443 ssl http2; - server_name your-domain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - location /api/v1/ws/ { - proxy_pass http://localhost:5006; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - # ... other headers - } -} -``` - -## Conclusion - -LNbits WebSocket implementation provides a robust foundation for real-time wallet monitoring and payment processing. By following this guide, you can implement reliable WebSocket connections that enhance user experience with instant payment notifications and live wallet updates. - -Remember to implement proper error handling, reconnection logic, and security measures when deploying to production environments. \ No newline at end of file diff --git a/misc-aio/lnbits-websocket-guide.pdf b/misc-aio/lnbits-websocket-guide.pdf deleted file mode 100644 index 38fb0666..00000000 Binary files a/misc-aio/lnbits-websocket-guide.pdf and /dev/null differ diff --git a/misc-aio/publish_profiles_from_csv.py b/misc-aio/publish_profiles_from_csv.py deleted file mode 100644 index b33ab666..00000000 --- a/misc-aio/publish_profiles_from_csv.py +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env python3 -""" -Bulk publish Nostr profiles from CSV file -Usage: python publish_profiles_from_csv.py -""" -import sys -import csv -import json -import time -import hashlib -import secp256k1 -from websocket import create_connection - -def sign_event(event, public_key_hex, private_key): - """Sign a Nostr event""" - # Create the signature data - signature_data = json.dumps([ - 0, - public_key_hex, - event["created_at"], - event["kind"], - event["tags"], - event["content"] - ], separators=(',', ':'), ensure_ascii=False) - - # Calculate event ID - event_id = hashlib.sha256(signature_data.encode()).hexdigest() - event["id"] = event_id - event["pubkey"] = public_key_hex - - # Sign the event - signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex() - event["sig"] = signature - - return event - -def publish_profile_metadata(private_key_hex, profile_name, relay_url): - """Publish a Nostr kind 0 metadata event""" - try: - # Convert hex private key to secp256k1 PrivateKey and get public key - private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex)) - public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix - - print(f"Publishing profile for: {profile_name}") - print(f" Public key: {public_key_hex}") - - # Create Nostr kind 0 metadata event - metadata = { - "name": profile_name, - "display_name": profile_name, - "about": f"Profile for {profile_name}" - } - - event = { - "kind": 0, - "created_at": int(time.time()), - "tags": [], - "content": json.dumps(metadata, separators=(',', ':')) - } - - # Sign the event - signed_event = sign_event(event, public_key_hex, private_key) - - # Connect to relay and publish - ws = create_connection(relay_url, timeout=15) - - # Send the event - event_message = f'["EVENT",{json.dumps(signed_event)}]' - ws.send(event_message) - - # Wait for response - try: - response = ws.recv() - print(f" ✅ Published successfully: {response}") - except Exception as e: - print(f" ⚠️ No immediate response: {e}") - - # Close connection - ws.close() - return True - - except Exception as e: - print(f" ❌ Failed to publish: {e}") - return False - -def main(): - if len(sys.argv) != 3: - print("Usage: python publish_profiles_from_csv.py ") - print("Example: python publish_profiles_from_csv.py publish-these.csv wss://relay.example.com") - sys.exit(1) - - csv_file = sys.argv[1] - relay_url = sys.argv[2] - - print(f"Publishing profiles from {csv_file} to {relay_url}") - print("=" * 60) - - published_count = 0 - failed_count = 0 - - try: - with open(csv_file, 'r') as file: - csv_reader = csv.DictReader(file) - - for row_num, row in enumerate(csv_reader, start=2): # start=2 because header is line 1 - username = row['username'].strip() - private_key_hex = row['prvkey'].strip() - - if not username or not private_key_hex: - print(f"Row {row_num}: Skipping empty row") - continue - - print(f"\nRow {row_num}: Processing {username}") - - success = publish_profile_metadata(private_key_hex, username, relay_url) - - if success: - published_count += 1 - else: - failed_count += 1 - - # Small delay between publishes to be nice to the relay - time.sleep(1) - - except FileNotFoundError: - print(f"❌ File not found: {csv_file}") - sys.exit(1) - except Exception as e: - print(f"❌ Error processing CSV: {e}") - sys.exit(1) - - print("\n" + "=" * 60) - print(f"Publishing complete!") - print(f"✅ Successfully published: {published_count}") - print(f"❌ Failed: {failed_count}") - print(f"📊 Total processed: {published_count + failed_count}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/misc-aio/test_nostr_connection.py b/misc-aio/test_nostr_connection.py deleted file mode 100644 index d00b75b7..00000000 --- a/misc-aio/test_nostr_connection.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -""" -Manual Nostr profile metadata publisher -Usage: python test_nostr_connection.py -""" -import sys -import json -import time -import hashlib -import secp256k1 -from websocket import create_connection - -def sign_event(event, public_key_hex, private_key): - """Sign a Nostr event""" - # Create the signature data - signature_data = json.dumps([ - 0, - public_key_hex, - event["created_at"], - event["kind"], - event["tags"], - event["content"] - ], separators=(',', ':'), ensure_ascii=False) - - # Calculate event ID - event_id = hashlib.sha256(signature_data.encode()).hexdigest() - event["id"] = event_id - event["pubkey"] = public_key_hex - - # Sign the event - signature = private_key.schnorr_sign(bytes.fromhex(event_id), None, raw=True).hex() - event["sig"] = signature - - return event - -def publish_profile_metadata(private_key_hex, profile_name, relay_url): - """Publish a Nostr kind 0 metadata event""" - try: - # Convert hex private key to secp256k1 PrivateKey and get public key - private_key = secp256k1.PrivateKey(bytes.fromhex(private_key_hex)) - public_key_hex = private_key.pubkey.serialize()[1:].hex() # Remove the 0x02 prefix - - print(f"Private key: {private_key_hex}") - print(f"Public key: {public_key_hex}") - print(f"Profile name: {profile_name}") - print(f"Relay URL: {relay_url}") - print() - - # Create Nostr kind 0 metadata event - metadata = { - "name": profile_name, - "display_name": profile_name, - "about": f"Manual profile update for {profile_name}" - } - - event = { - "kind": 0, - "created_at": int(time.time()), - "tags": [], - "content": json.dumps(metadata, separators=(',', ':')) - } - - # Sign the event - signed_event = sign_event(event, public_key_hex, private_key) - - print(f"Signed event: {json.dumps(signed_event, indent=2)}") - print() - - # Connect to relay and publish - print(f"Connecting to relay: {relay_url}") - ws = create_connection(relay_url, timeout=15) - print("✅ Connected successfully!") - - # Send the event - event_message = f'["EVENT",{json.dumps(signed_event)}]' - print(f"Sending EVENT: {event_message}") - ws.send(event_message) - - # Wait for response - try: - response = ws.recv() - print(f"✅ Relay response: {response}") - except Exception as e: - print(f"⚠️ No immediate response: {e}") - - # Close connection - ws.close() - print("✅ Connection closed successfully") - print(f"Profile metadata for '{profile_name}' published successfully!") - - except Exception as e: - print(f"❌ Failed to publish profile: {e}") - raise - -if __name__ == "__main__": - if len(sys.argv) != 4: - print("Usage: python test_nostr_connection.py ") - print("Example: python test_nostr_connection.py abc123... 'My Name' wss://relay.example.com") - sys.exit(1) - - private_key_hex = sys.argv[1] - profile_name = sys.argv[2] - relay_url = sys.argv[3] - - publish_profile_metadata(private_key_hex, profile_name, relay_url) \ No newline at end of file