feat: add external id for users (#3219)

This commit is contained in:
Vlad Stan
2025-07-08 10:17:16 +02:00
committed by dni ⚡
parent ff24847980
commit c7b7832a88
11 changed files with 458 additions and 25 deletions
+13 -8
View File
@@ -68,6 +68,7 @@ async def get_accounts(
accounts.username,
accounts.email,
accounts.pubkey,
accounts.external_id,
SUM(COALESCE((
SELECT balance FROM balances WHERE wallet_id = wallets.id
), 0)) as balance_msat,
@@ -128,8 +129,8 @@ async def get_account_by_username(
if len(username) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE username = :username",
{"username": username},
"SELECT * FROM accounts WHERE LOWER(username) = :username",
{"username": username.lower()},
Account,
)
@@ -138,8 +139,8 @@ async def get_account_by_pubkey(
pubkey: str, conn: Optional[Connection] = None
) -> Optional[Account]:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE pubkey = :pubkey",
{"pubkey": pubkey},
"SELECT * FROM accounts WHERE LOWER(pubkey) = :pubkey",
{"pubkey": pubkey.lower()},
Account,
)
@@ -150,8 +151,8 @@ async def get_account_by_email(
if len(email) == 0:
return None
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE email = :email",
{"email": email},
"SELECT * FROM accounts WHERE LOWER(email) = :email",
{"email": email.lower()},
Account,
)
@@ -160,8 +161,11 @@ async def get_account_by_username_or_email(
username_or_email: str, conn: Optional[Connection] = None
) -> Optional[Account]:
return await (conn or db).fetchone(
"SELECT * FROM accounts WHERE email = :value or username = :value",
{"value": username_or_email},
"""
SELECT * FROM accounts
WHERE LOWER(email) = :value or LOWER(username) = :value
""",
{"value": username_or_email.lower()},
Account,
)
@@ -183,6 +187,7 @@ async def get_user_from_account(
email=account.email,
username=account.username,
pubkey=account.pubkey,
external_id=account.external_id,
extra=account.extra,
created_at=account.created_at,
updated_at=account.updated_at,
+8
View File
@@ -711,3 +711,11 @@ async def m031_add_color_and_icon_to_wallets(db: Connection):
Adds icon and color columns to wallets.
"""
await db.execute("ALTER TABLE wallets ADD COLUMN extra TEXT")
async def m032_add_external_id_to_accounts(db: Connection):
"""
Adds external_id column to accounts.
Used for external account linking.
"""
await db.execute("ALTER TABLE accounts ADD COLUMN external_id TEXT")
+23 -3
View File
@@ -9,7 +9,12 @@ from pydantic import BaseModel, Field
from lnbits.core.models.misc import SimpleItem
from lnbits.db import FilterModel
from lnbits.helpers import is_valid_email_address, is_valid_pubkey, is_valid_username
from lnbits.helpers import (
is_valid_email_address,
is_valid_external_id,
is_valid_pubkey,
is_valid_username,
)
from lnbits.settings import settings
from .wallets import Wallet
@@ -93,6 +98,7 @@ class UserAcls(BaseModel):
class Account(BaseModel):
id: str
external_id: str | None = None # for external account linking
username: str | None = None
password_hash: str | None = None
pubkey: str | None = None
@@ -130,6 +136,11 @@ class Account(BaseModel):
raise ValueError("Invalid email.")
if self.pubkey and not is_valid_pubkey(self.pubkey):
raise ValueError("Invalid pubkey.")
if self.external_id and not is_valid_external_id(self.external_id):
raise ValueError(
"Invalid external id. Max length is 256 characters. "
"Space and newlines are not allowed."
)
user_uuid4 = UUID(hex=self.id, version=4)
if user_uuid4.hex != self.id:
raise ValueError("User ID is not valid UUID4 hex string.")
@@ -143,7 +154,14 @@ class AccountOverview(Account):
class AccountFilters(FilterModel):
__search_fields__ = ["user", "email", "username", "pubkey", "wallet_id"]
__search_fields__ = [
"user",
"email",
"username",
"pubkey",
"external_id",
"wallet_id",
]
__sort_fields__ = [
"balance_msat",
"email",
@@ -157,6 +175,7 @@ class AccountFilters(FilterModel):
user: str | None = None
username: str | None = None
pubkey: str | None = None
external_id: str | None = None
wallet_id: str | None = None
@@ -167,6 +186,7 @@ class User(BaseModel):
email: str | None = None
username: str | None = None
pubkey: str | None = None
external_id: str | None = None # for external account linking
extensions: list[str] = []
wallets: list[Wallet] = []
admin: bool = False
@@ -207,13 +227,13 @@ class CreateUser(BaseModel):
password: str | None = Query(default=None, min_length=8, max_length=50)
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
pubkey: str = Query(default=None, max_length=64)
external_id: str = Query(default=None, max_length=256)
extensions: list[str] | None = None
extra: UserExtra | None = None
class UpdateUser(BaseModel):
user_id: str
email: str | None = Query(default=None)
username: str | None = Query(default=..., min_length=2, max_length=20)
extra: UserExtra | None = None
+11
View File
@@ -272,10 +272,21 @@
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.external_id"
:label="$t('external_id')"
filled
dense
readonly
class="q-mb-md"
>
</q-input>
<q-input
v-model="user.extra.picture"
:label="$t('picture')"
filled
dense
class="q-mb-md"
>
</q-input>
@@ -147,6 +147,14 @@
class="q-mb-md"
>
</q-input>
<q-input
v-model="activeUser.data.external_id"
:label="$t('external_id')"
filled
dense
class="q-mb-md"
>
</q-input>
<q-input
v-model="activeUser.data.extra.picture"
:label="$t('picture')"
+1 -13
View File
@@ -411,23 +411,13 @@ async def update(
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid user ID.")
if data.username and not is_valid_username(data.username):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Invalid username.")
if data.email != user.email:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Email mismatch.",
)
if (
data.username
and user.username != data.username
and await get_account_by_username(data.username)
):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Username already exists.")
if (
data.email
and data.email != user.email
and await get_account_by_email(data.email)
):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Email already exists.")
account = await get_account(user.id)
if not account:
@@ -435,8 +425,6 @@ async def update(
if data.username:
account.username = data.username
if data.email:
account.email = data.email
if data.extra:
account.extra = data.extra
+2
View File
@@ -100,6 +100,7 @@ async def api_create_user(data: CreateUser) -> CreateUser:
username=data.username,
email=data.email,
pubkey=data.pubkey,
external_id=data.external_id,
extra=data.extra,
)
account.validate_fields()
@@ -132,6 +133,7 @@ async def api_update_user(
username=data.username,
email=data.email,
pubkey=data.pubkey,
external_id=data.external_id,
extra=data.extra or UserExtra(),
)
await update_user_account(account)
+8
View File
@@ -198,6 +198,14 @@ def is_valid_username(username: str) -> bool:
return re.fullmatch(username_regex, username) is not None
def is_valid_external_id(external_id: str) -> bool:
if len(external_id) > 256:
return False
if " " in external_id or "\n" in external_id:
return False
return True
def is_valid_pubkey(pubkey: str) -> bool:
if len(pubkey) != 64:
return False
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -350,6 +350,7 @@ window.localisation.en = {
update_account: 'Update Account',
invalid_username: 'Invalid Username',
auth_provider: 'Auth Provider',
external_id: 'External ID',
my_account: 'My Account',
existing_account_question: 'Already have an account?',
background_image: 'Background Image',