Compare commits

..
Author SHA1 Message Date
Vlad Stan c5388e23a9 fix: settings reset 2026-02-04 14:05:19 +02:00
Vlad Stan d01893f577 test: one time code 2026-02-04 14:04:25 +02:00
Vlad Stan 1fc15cde81 test: invalid code, reusable code 2026-02-04 13:57:46 +02:00
Vlad Stan c711be4d60 test: no code provided 2026-02-04 13:48:46 +02:00
Vlad Stan ccb9dda164 fix: handle empty strings better 2026-02-04 13:11:40 +02:00
Vlad Stan 100bc45185 chore: code clean-up 2026-02-03 18:11:04 +02:00
Vlad Stan 222d1c8990 fix: remove nostr message 2026-02-03 18:11:04 +02:00
Vlad Stan 69c414fabf feat: better wording 2026-02-03 18:11:04 +02:00
Vlad Stan b33aa41b9a feat: check invitation code 2026-02-03 18:11:04 +02:00
Vlad Stan 68a36dfd18 feat: update settings 2026-02-03 18:11:04 +02:00
Vlad Stan 84a958af6e feat: send invitation code to backend 2026-02-03 18:11:04 +02:00
Vlad Stan 7db645d388 feat: add confirmation options UI 2026-02-03 18:11:04 +02:00
Vlad Stan 30e61eacf3 feat: configure ui 2026-02-03 18:11:04 +02:00
Vlad Stan de496ed9e8 feat: configure activation codes 2026-02-03 18:11:04 +02:00
Vlad Stan 8cb35d9aac feat: add some info 2026-02-03 18:11:04 +02:00
Vlad Stan d662b03370 refactor: reorder fields 2026-02-03 18:11:04 +02:00
Vlad Stan ef78ca7db3 feat: add ui config 2026-02-03 18:11:04 +02:00
Vlad Stan c97c5842ad refactor: better query 2026-02-03 18:10:41 +02:00
Vlad Stan abce6196de refactor: code cleanup 2026-02-03 18:10:41 +02:00
Vlad Stan e7d3be9854 chore: clean-up 2026-02-03 18:10:41 +02:00
Vlad Stan e9dc8c775d chore: bundle 2026-02-03 18:10:41 +02:00
Vlad Stan 31a2d68285 refactor: simplify queries 2026-02-03 18:10:41 +02:00
Vlad Stan bf1ad185b9 refactor: use normal update 2026-02-03 18:10:41 +02:00
Vlad Stan 62de35541c test: add more check 2026-02-03 18:10:41 +02:00
Vlad Stan 1a61435868 test: login after user disabled does not work 2026-02-03 18:10:41 +02:00
Vlad Stan 03ba28d0cb fix: column selection 2026-02-03 18:10:41 +02:00
Vlad Stan 2b260bfdaf fix: better message 2026-02-03 18:10:41 +02:00
Vlad Stan 30ab42c0b2 feat: add back toggle admin 2026-02-03 18:10:41 +02:00
Vlad Stan f603026d5e fix: only fetch keys for activated users 2026-02-03 18:10:41 +02:00
Vlad Stan 84e6b3a9a8 fix: clear cache 2026-02-03 18:10:41 +02:00
Vlad Stan 3aafc0724f feat: clear cache on user deactivation 2026-02-03 18:10:41 +02:00
Vlad Stan b978ce11fc feat: basic account activate/deactivate 2026-02-03 18:10:41 +02:00
7 changed files with 50 additions and 31 deletions
+42 -9
View File
@@ -12,6 +12,7 @@ from typing import Any, Generic, Literal, TypeVar, get_origin
from loguru import logger
from pydantic import BaseModel, ValidationError, root_validator
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from sqlalchemy.sql import text
@@ -55,6 +56,14 @@ def compat_timestamp_placeholder(key: str):
return f":{key}"
def get_placeholder(model: Any, field: str) -> str:
type_ = model.__fields__[field].type_
if type_ == datetime:
return compat_timestamp_placeholder(field)
else:
return f":{field}"
class Compat:
type: str | None = "<inherited>"
schema: str | None = "<inherited>"
@@ -317,7 +326,31 @@ class Database(Compat):
self.engine: AsyncEngine = create_async_engine(
database_uri, echo=settings.debug_database
)
if self.type in {POSTGRES, COCKROACH}:
@event.listens_for(self.engine.sync_engine, "connect")
def register_custom_types(dbapi_connection, *_):
def _parse_date(value) -> datetime:
if value is None:
value = "1970-01-01 00:00:00"
f = "%Y-%m-%d %H:%M:%S.%f"
if "." not in value:
f = "%Y-%m-%d %H:%M:%S"
# Parse and add UTC timezone info
return datetime.strptime(value, f).replace(tzinfo=timezone.utc)
dbapi_connection.run_async(
lambda connection: connection.set_type_codec(
"TIMESTAMP",
encoder=datetime,
decoder=_parse_date,
schema="pg_catalog",
)
)
self.lock = asyncio.Lock()
logger.trace(f"database {self.type} added for {self.name}")
@asynccontextmanager
@@ -630,7 +663,7 @@ def insert_query(table_name: str, model: BaseModel) -> str:
placeholders = []
keys = model_to_dict(model).keys()
for field in keys:
placeholders.append(f":{field}")
placeholders.append(get_placeholder(model, field))
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields = ", ".join([f'"{key}"' for key in keys])
values = ", ".join(placeholders)
@@ -648,8 +681,9 @@ def update_query(
"""
fields = []
for field in model_to_dict(model).keys():
placeholder = get_placeholder(model, field)
# add quotes to keys to avoid SQL conflicts (e.g. `user` is a reserved keyword)
fields.append(f'"{field}" = :{field}')
fields.append(f'"{field}" = {placeholder}')
query = ", ".join(fields)
return f"UPDATE {table_name} SET {query} {where}" # noqa: S608
@@ -667,12 +701,7 @@ def model_to_dict(model: BaseModel) -> dict:
if model.__fields__[key].field_info.extra.get("no_database", False):
continue
if isinstance(value, datetime):
if DB_TYPE == SQLITE:
_dict[key] = value.timestamp()
else:
# remove tz. postgres and cockroach TIMESTAMP is not tz aware
# so it will throw if we dont remove the UTC.
_dict[key] = value.replace(tzinfo=None)
_dict[key] = value.timestamp()
continue
if (
type(type_) is type(BaseModel)
@@ -728,7 +757,11 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: # noqa: C901
if DB_TYPE == SQLITE:
_dict[key] = datetime.fromtimestamp(value, timezone.utc)
else:
_dict[key] = value.replace(tzinfo=timezone.utc)
# Ensure PostgreSQL datetime values have timezone info
if isinstance(value, datetime) and value.tzinfo is None:
_dict[key] = value.replace(tzinfo=timezone.utc)
else:
_dict[key] = value
continue
if issubclass(type_, BaseModel):
_dict[key] = dict_to_submodel(type_, value)
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -71,10 +71,6 @@ window.app.component('lnbits-wallet-extra', {
'lnbits.exchangeRate.' + this.g.wallet.currency,
this.g.exchangeRate
)
if (this.g.exchangeRate <= 0) {
this.g.fiatTracking = false
this.g.isFiatPriority = false
}
})
.catch(e => console.error(e))
}
+1 -5
View File
@@ -144,7 +144,7 @@ window._lnbitsUtils = {
return null
}
},
async notifyApiError(error) {
notifyApiError(error) {
if (!error.response) {
return console.error(error)
}
@@ -154,10 +154,6 @@ window._lnbitsUtils = {
500: 'negative'
}
let messages = error.response.data.detail
if (!messages) {
const text = await error.response.data?.text()
messages = this.parseJSONSafe(text)?.detail
}
if (messages) {
messages = Array.isArray(messages)
? messages.map(e => e.msg + ` (${e.loc?.join('/')})`)
+1 -1
View File
@@ -178,7 +178,7 @@
"
:clickable="!!reviewsUrl"
@click="openReviews(extension)"
></lnbits-extension-rating>
/>
<q-btn-group size="xs" style="margin: 5px 0">
<q-btn
v-if="extension.hasFreeRelease"
+4 -10
View File
@@ -292,8 +292,7 @@ async def btc_rates(currency: str) -> list[tuple[str, float]]:
async def btc_price(currency: str) -> float:
rates = await btc_rates(currency)
if not rates:
logger.warning("Could not fetch any Bitcoin price.")
return 0.0
raise ValueError("Could not fetch any Bitcoin price.")
elif len(rates) == 1:
logger.warning("Could only fetch one Bitcoin price.")
@@ -307,8 +306,7 @@ async def get_fiat_rate_and_price_satoshis(currency: str) -> tuple[float, float]
f"btc-price-{currency}",
settings.lnbits_exchange_rate_cache_seconds,
)
rate = float(100_000_000 / price) if price > 0 else 0.0
return rate, price
return float(100_000_000 / price), price
async def get_fiat_rate_satoshis(currency: str) -> float:
@@ -318,13 +316,9 @@ async def get_fiat_rate_satoshis(currency: str) -> float:
async def fiat_amount_as_satoshis(amount: float, currency: str) -> int:
rate = await get_fiat_rate_satoshis(currency)
if rate > 0:
return int(amount * rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
return int(amount * (rate))
async def satoshis_amount_as_fiat(amount: float, currency: str) -> float:
rate = await get_fiat_rate_satoshis(currency)
if rate > 0:
return float(amount / rate)
raise ValueError(f"Could not get exchange rate for {currency}.")
return float(amount / rate)