[feat] add default_user_extensions setting (#2571)
* feat: add `lnbits_user_default_extensions` to `settings` * refactor: extract `create_user_account` in services * feat: auto enable user extensions
This commit is contained in:
parent
fb17611207
commit
b2564154cd
7 changed files with 143 additions and 131 deletions
|
|
@ -162,6 +162,8 @@ LNBITS_ADMIN_USERS=""
|
||||||
|
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||||
|
# Extensions enabled by default when a user is created
|
||||||
|
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||||
|
|
||||||
# Start LNbits core only. The extensions are not loaded.
|
# Start LNbits core only. The extensions are not loaded.
|
||||||
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
|
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
import json
|
import json
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Dict, List, Literal, Optional
|
from typing import Any, Dict, List, Literal, Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
@ -26,7 +26,6 @@ from lnbits.settings import (
|
||||||
from .models import (
|
from .models import (
|
||||||
Account,
|
Account,
|
||||||
AccountFilters,
|
AccountFilters,
|
||||||
CreateUser,
|
|
||||||
Payment,
|
Payment,
|
||||||
PaymentFilters,
|
PaymentFilters,
|
||||||
PaymentHistoryPoint,
|
PaymentHistoryPoint,
|
||||||
|
|
@ -42,63 +41,23 @@ from .models import (
|
||||||
# --------
|
# --------
|
||||||
|
|
||||||
|
|
||||||
async def create_user(
|
|
||||||
data: CreateUser, user_config: Optional[UserConfig] = None
|
|
||||||
) -> User:
|
|
||||||
if not settings.new_accounts_allowed:
|
|
||||||
raise ValueError("Account creation is disabled.")
|
|
||||||
if await get_account_by_username(data.username):
|
|
||||||
raise ValueError("Username already exists.")
|
|
||||||
|
|
||||||
if data.email and await get_account_by_email(data.email):
|
|
||||||
raise ValueError("Email already exists.")
|
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
user_id = uuid4().hex
|
|
||||||
tsph = db.timestamp_placeholder
|
|
||||||
now = int(time())
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
INSERT INTO accounts
|
|
||||||
(id, email, username, pass, extra, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, {tsph}, {tsph})
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
user_id,
|
|
||||||
data.email,
|
|
||||||
data.username,
|
|
||||||
pwd_context.hash(data.password),
|
|
||||||
json.dumps(dict(user_config)) if user_config else "{}",
|
|
||||||
now,
|
|
||||||
now,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
new_account = await get_account(user_id=user_id)
|
|
||||||
assert new_account, "Newly created account couldn't be retrieved"
|
|
||||||
return new_account
|
|
||||||
|
|
||||||
|
|
||||||
async def create_account(
|
async def create_account(
|
||||||
conn: Optional[Connection] = None,
|
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
email: Optional[str] = None,
|
email: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
user_config: Optional[UserConfig] = None,
|
user_config: Optional[UserConfig] = None,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
) -> User:
|
) -> User:
|
||||||
if user_id:
|
user_id = user_id or uuid4().hex
|
||||||
user_uuid4 = UUID(hex=user_id, version=4)
|
|
||||||
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
|
|
||||||
else:
|
|
||||||
user_id = uuid4().hex
|
|
||||||
|
|
||||||
extra = json.dumps(dict(user_config)) if user_config else "{}"
|
extra = json.dumps(dict(user_config)) if user_config else "{}"
|
||||||
now = int(time())
|
now = int(time())
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO accounts (id, email, extra, created_at, updated_at)
|
INSERT INTO accounts (id, username, pass, email, extra, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder})
|
||||||
""",
|
""",
|
||||||
(user_id, email, extra, now, now),
|
(user_id, username, password, email, extra, now, now),
|
||||||
)
|
)
|
||||||
|
|
||||||
new_account = await get_account(user_id=user_id, conn=conn)
|
new_account = await get_account(user_id=user_id, conn=conn)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple, TypedDict
|
from typing import Dict, List, Optional, Tuple, TypedDict
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from fastapi import Depends, WebSocket
|
from fastapi import Depends, WebSocket
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from passlib.context import CryptContext
|
||||||
from py_vapid import Vapid
|
from py_vapid import Vapid
|
||||||
from py_vapid.utils import b64urlencode
|
from py_vapid.utils import b64urlencode
|
||||||
|
|
||||||
|
|
@ -50,6 +52,8 @@ from .crud import (
|
||||||
create_wallet,
|
create_wallet,
|
||||||
delete_wallet_payment,
|
delete_wallet_payment,
|
||||||
get_account,
|
get_account,
|
||||||
|
get_account_by_email,
|
||||||
|
get_account_by_username,
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
get_super_settings,
|
get_super_settings,
|
||||||
|
|
@ -60,9 +64,10 @@ from .crud import (
|
||||||
update_payment_details,
|
update_payment_details,
|
||||||
update_payment_status,
|
update_payment_status,
|
||||||
update_super_user,
|
update_super_user,
|
||||||
|
update_user_extension,
|
||||||
)
|
)
|
||||||
from .helpers import to_valid_user_id
|
from .helpers import to_valid_user_id
|
||||||
from .models import BalanceDelta, Payment, UserConfig, Wallet
|
from .models import BalanceDelta, Payment, User, UserConfig, Wallet
|
||||||
|
|
||||||
|
|
||||||
class PaymentError(Exception):
|
class PaymentError(Exception):
|
||||||
|
|
@ -775,6 +780,38 @@ async def init_admin_settings(super_user: Optional[str] = None) -> SuperSettings
|
||||||
return await create_admin_settings(account.id, editable_settings.dict())
|
return await create_admin_settings(account.id, editable_settings.dict())
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user_account(
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
user_config: Optional[UserConfig] = None,
|
||||||
|
) -> User:
|
||||||
|
if not settings.new_accounts_allowed:
|
||||||
|
raise ValueError("Account creation is disabled.")
|
||||||
|
if username and await get_account_by_username(username):
|
||||||
|
raise ValueError("Username already exists.")
|
||||||
|
|
||||||
|
if email and await get_account_by_email(email):
|
||||||
|
raise ValueError("Email already exists.")
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
user_uuid4 = UUID(hex=user_id, version=4)
|
||||||
|
assert user_uuid4.hex == user_id, "User ID is not valid UUID4 hex string"
|
||||||
|
else:
|
||||||
|
user_id = uuid4().hex
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
password = pwd_context.hash(password) if password else None
|
||||||
|
|
||||||
|
account = await create_account(user_id, username, email, password, user_config)
|
||||||
|
|
||||||
|
for ext_id in settings.lnbits_user_default_extensions:
|
||||||
|
await update_user_extension(user_id=account.id, extension=ext_id, active=True)
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
class WebsocketConnectionManager:
|
class WebsocketConnectionManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.active_connections: List[WebSocket] = []
|
self.active_connections: List[WebSocket] = []
|
||||||
|
|
|
||||||
|
|
@ -45,56 +45,7 @@
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p>Admin Extensions</p>
|
|
||||||
<q-select
|
|
||||||
filled
|
|
||||||
v-model="formData.lnbits_admin_extensions"
|
|
||||||
multiple
|
|
||||||
hint="Extensions only user with admin privileges can use"
|
|
||||||
label="Admin extensions"
|
|
||||||
:options="g.extensions.map(e => e.code)"
|
|
||||||
></q-select>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<p>Miscellaneous</p>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Disable Extensions</q-item-label>
|
|
||||||
<q-item-label caption>Disables all extensions</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_extensions_deactivate_all"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item tag="label" v-ripple>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>Hide API</q-item-label>
|
|
||||||
<q-item-label caption
|
|
||||||
>Hides wallet api, extensions can choose to honor</q-item-label
|
|
||||||
>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-toggle
|
|
||||||
size="md"
|
|
||||||
v-model="formData.lnbits_hide_api"
|
|
||||||
checked-icon="check"
|
|
||||||
color="green"
|
|
||||||
unchecked-icon="clear"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
<h6 class="q-my-none">Service Fee</h6>
|
<h6 class="q-my-none">Service Fee</h6>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
|
|
@ -154,32 +105,94 @@
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<q-separator></q-separator>
|
||||||
<h6 class="q-my-none">Extensions</h6>
|
<h6 class="q-my-none">Extensions</h6>
|
||||||
<div>
|
<div class="row q-col-gutter-md">
|
||||||
<p>Extension Sources</p>
|
<div class="col-12">
|
||||||
<q-input
|
<p>Extension Sources</p>
|
||||||
filled
|
<q-input
|
||||||
v-model="formAddExtensionsManifest"
|
filled
|
||||||
@keydown.enter="addExtensionsManifest"
|
v-model="formAddExtensionsManifest"
|
||||||
type="text"
|
@keydown.enter="addExtensionsManifest"
|
||||||
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
type="text"
|
||||||
hint="Repositories from where the extensions can be downloaded"
|
label="Source URL (only use the official LNbits extension source, and sources you can trust)"
|
||||||
>
|
hint="Repositories from where the extensions can be downloaded"
|
||||||
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
>
|
||||||
</q-input>
|
<q-btn @click="addExtensionsManifest" dense flat icon="add"></q-btn>
|
||||||
<div>
|
</q-input>
|
||||||
<q-chip
|
<div>
|
||||||
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
<q-chip
|
||||||
:key="manifestUrl"
|
v-for="manifestUrl in formData.lnbits_extensions_manifests"
|
||||||
removable
|
:key="manifestUrl"
|
||||||
@remove="removeExtensionsManifest(manifestUrl)"
|
removable
|
||||||
color="primary"
|
@remove="removeExtensionsManifest(manifestUrl)"
|
||||||
text-color="white"
|
color="primary"
|
||||||
><span v-text="manifestUrl"></span
|
text-color="white"
|
||||||
></q-chip>
|
><span v-text="manifestUrl"></span
|
||||||
|
></q-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Admin Extensions</p>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_admin_extensions"
|
||||||
|
multiple
|
||||||
|
hint="Extensions only user with admin privileges can use"
|
||||||
|
label="Admin extensions"
|
||||||
|
:options="g.extensions.map(e => e.code)"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>User Default Extensions</p>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_user_default_extensions"
|
||||||
|
multiple
|
||||||
|
hint="Extensions that will be enabled by default for the users."
|
||||||
|
label="User extensions"
|
||||||
|
:options="g.extensions.map(e => e.code)"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<p>Miscellaneous</p>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Disable Extensions</q-item-label>
|
||||||
|
<q-item-label caption>Disables all extensions</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_extensions_deactivate_all"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Hide API</q-item-label>
|
||||||
|
<q-item-label caption
|
||||||
|
>Hides wallet api, extensions can choose to honor</q-item-label
|
||||||
|
>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_hide_api"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,9 @@ from lnbits.utils.exchange_rates import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_account,
|
|
||||||
create_wallet,
|
create_wallet,
|
||||||
)
|
)
|
||||||
from ..services import perform_lnurlauth
|
from ..services import create_user_account, perform_lnurlauth
|
||||||
|
|
||||||
# backwards compatibility for extension
|
# backwards compatibility for extension
|
||||||
# TODO: remove api_payment and pay_invoice imports from extensions
|
# TODO: remove api_payment and pay_invoice imports from extensions
|
||||||
|
|
@ -70,7 +69,7 @@ async def api_create_account(data: CreateWallet) -> Wallet:
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
detail="Account creation is disabled.",
|
detail="Account creation is disabled.",
|
||||||
)
|
)
|
||||||
account = await create_account()
|
account = await create_user_account()
|
||||||
return await create_wallet(user_id=account.id, wallet_name=data.name)
|
return await create_wallet(user_id=account.id, wallet_name=data.name)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from starlette.status import (
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from lnbits.core.services import create_user_account
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
from lnbits.helpers import (
|
from lnbits.helpers import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
|
|
@ -23,8 +24,6 @@ from lnbits.helpers import (
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
|
|
||||||
from ..crud import (
|
from ..crud import (
|
||||||
create_account,
|
|
||||||
create_user,
|
|
||||||
get_account,
|
get_account,
|
||||||
get_account_by_email,
|
get_account_by_email,
|
||||||
get_account_by_username_or_email,
|
get_account_by_username_or_email,
|
||||||
|
|
@ -166,7 +165,9 @@ async def register(data: CreateUser) -> JSONResponse:
|
||||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
|
raise HTTPException(HTTP_400_BAD_REQUEST, "Invalid email.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = await create_user(data)
|
user = await create_user_account(
|
||||||
|
email=data.email, username=data.username, password=data.password
|
||||||
|
)
|
||||||
return _auth_success_response(user.username)
|
return _auth_success_response(user.username)
|
||||||
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|
@ -274,7 +275,7 @@ async def _handle_sso_login(userinfo: OpenID, verified_user_id: Optional[str] =
|
||||||
else:
|
else:
|
||||||
if not settings.new_accounts_allowed:
|
if not settings.new_accounts_allowed:
|
||||||
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
|
raise HTTPException(HTTP_400_BAD_REQUEST, "Account creation is disabled.")
|
||||||
user = await create_account(email=email, user_config=user_config)
|
user = await create_user_account(email=email, user_config=user_config)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
|
raise HTTPException(HTTP_401_UNAUTHORIZED, "User not found.")
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class UsersSettings(LNbitsSettings):
|
||||||
|
|
||||||
class ExtensionsSettings(LNbitsSettings):
|
class ExtensionsSettings(LNbitsSettings):
|
||||||
lnbits_admin_extensions: list[str] = Field(default=[])
|
lnbits_admin_extensions: list[str] = Field(default=[])
|
||||||
|
lnbits_user_default_extensions: list[str] = Field(default=[])
|
||||||
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
lnbits_extensions_deactivate_all: bool = Field(default=False)
|
||||||
lnbits_extensions_manifests: list[str] = Field(
|
lnbits_extensions_manifests: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue