From 815c3e61e4f534ed869b7dd15d86166dff3ababf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 21 Dec 2023 13:37:56 +0100 Subject: [PATCH] feat: add `created_at` and `updated_at` to wallets and accounts (#2177) * feat: add `created_at` and `updated_at` to wallets and accounts the title says it all :) * fixup! * nitpicks :) * fixup! * sqlite fix * sqlite compat * fixup! * mypy * revert db py * motorinas suggestions * int(time()) proper default values in migration * uncomment migration * use now = int(time()) idiom to make code more readable also this fixes the issue where time() is called multiple times providing different return values for multiple invocations --------- Co-authored-by: Pavol Rusnak --- lnbits/core/crud.py | 80 +++++++++++++++++++++++++++++---------- lnbits/core/migrations.py | 67 ++++++++++++++++++++++++++++++++ lnbits/core/models.py | 4 ++ lnbits/db.py | 6 +++ 4 files changed, 137 insertions(+), 20 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 1e51bfd1..7d077ad6 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -1,5 +1,6 @@ import datetime import json +from time import time from typing import Any, Dict, List, Literal, Optional from urllib.parse import urlparse from uuid import UUID, uuid4 @@ -51,10 +52,13 @@ async def create_user( pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") user_id = uuid4().hex + tsph = db.timestamp_placeholder + now = int(time()) await db.execute( - """ - INSERT INTO accounts (id, email, username, pass, extra) - VALUES (?, ?, ?, ?, ?) + f""" + INSERT INTO accounts + (id, email, username, pass, extra, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, {tsph}, {tsph}) """, ( user_id, @@ -62,6 +66,8 @@ async def create_user( 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) @@ -82,9 +88,13 @@ async def create_account( user_id = uuid4().hex extra = json.dumps(dict(user_config)) if user_config else "{}" + now = int(time()) await (conn or db).execute( - "INSERT INTO accounts (id, email, extra) VALUES (?, ?, ?)", - (user_id, email, extra), + f""" + INSERT INTO accounts (id, email, extra, created_at, updated_at) + VALUES (?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) + """, + (user_id, email, extra, now, now), ) new_account = await get_account(user_id=user_id, conn=conn) @@ -116,12 +126,20 @@ async def update_account( email = user.email or email extra = user_config or user.config + now = int(time()) await db.execute( - """ - UPDATE accounts SET (username, email, extra) = (?, ?, ?) + f""" + UPDATE accounts SET (username, email, extra, updated_at) = + (?, ?, ?, {db.timestamp_placeholder}) WHERE id = ? """, - (username, email, json.dumps(dict(extra)) if extra else "{}", user_id), + ( + username, + email, + json.dumps(dict(extra)) if extra else "{}", + now, + user_id, + ), ) user = await get_user(user_id) @@ -133,7 +151,7 @@ async def get_account( user_id: str, conn: Optional[Connection] = None ) -> Optional[User]: row = await (conn or db).fetchone( - "SELECT id, email, username FROM accounts WHERE id = ?", + "SELECT id, email, username, created_at, updated_at FROM accounts WHERE id = ?", (user_id,), ) @@ -172,10 +190,15 @@ async def update_user_password(data: UpdateUserPassword) -> Optional[User]: pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + now = int(time()) await db.execute( - "UPDATE accounts SET pass = ? WHERE id = ?", + f""" + UPDATE accounts SET pass = ?, updated_at = {db.timestamp_placeholder} + WHERE id = ? + """, ( pwd_context.hash(data.password), + now, data.user_id, ), ) @@ -189,7 +212,10 @@ async def get_account_by_username( username: str, conn: Optional[Connection] = None ) -> Optional[User]: row = await (conn or db).fetchone( - "SELECT id, username, email FROM accounts WHERE username = ?", + """ + SELECT id, username, email, created_at, updated_at + FROM accounts WHERE username = ? + """, (username,), ) @@ -200,7 +226,10 @@ async def get_account_by_email( email: str, conn: Optional[Connection] = None ) -> Optional[User]: row = await (conn or db).fetchone( - "SELECT id, username, email FROM accounts WHERE email = ?", + """ + SELECT id, username, email, created_at, updated_at + FROM accounts WHERE email = ? + """, (email,), ) @@ -218,7 +247,11 @@ async def get_account_by_username_or_email( async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[User]: user = await (conn or db).fetchone( - "SELECT id, email, username, pass, extra FROM accounts WHERE id = ?", (user_id,) + """ + SELECT id, email, username, pass, extra, created_at, updated_at + FROM accounts WHERE id = ? + """, + (user_id,), ) if user: @@ -392,10 +425,11 @@ async def create_wallet( conn: Optional[Connection] = None, ) -> Wallet: wallet_id = uuid4().hex + now = int(time()) await (conn or db).execute( - """ - INSERT INTO wallets (id, name, "user", adminkey, inkey) - VALUES (?, ?, ?, ?, ?) + f""" + INSERT INTO wallets (id, name, "user", adminkey, inkey, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, {db.timestamp_placeholder}, {db.timestamp_placeholder}) """, ( wallet_id, @@ -403,6 +437,8 @@ async def create_wallet( user_id, uuid4().hex, uuid4().hex, + now, + now, ), ) @@ -419,7 +455,10 @@ async def update_wallet( conn: Optional[Connection] = None, ) -> Optional[Wallet]: set_clause = [] - values = [] + values: list = [] + set_clause.append(f"updated_at = {db.timestamp_placeholder}") + now = int(time()) + values.append(now) if name: set_clause.append("name = ?") values.append(name) @@ -441,13 +480,14 @@ async def update_wallet( async def delete_wallet( *, user_id: str, wallet_id: str, conn: Optional[Connection] = None ) -> None: + now = int(time()) await (conn or db).execute( - """ + f""" UPDATE wallets - SET deleted = true + SET deleted = true, updated_at = {db.timestamp_placeholder} WHERE id = ? AND "user" = ? """, - (wallet_id, user_id), + (now, wallet_id, user_id), ) diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 4fad3708..b6457a12 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -1,4 +1,5 @@ import datetime +from time import time from loguru import logger from sqlalchemy.exc import OperationalError @@ -404,3 +405,69 @@ async def m016_add_username_column_to_accounts(db): await db.execute("ALTER TABLE accounts ADD COLUMN extra TEXT") except OperationalError: pass + + +async def m017_add_timestamp_columns_to_accounts_and_wallets(db): + """ + Adds created_at and updated_at column to accounts and wallets. + """ + try: + await db.execute( + "ALTER TABLE accounts " + f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE accounts " + f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE wallets " + f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + await db.execute( + "ALTER TABLE wallets " + f"ADD COLUMN updated_at TIMESTAMP DEFAULT {db.timestamp_column_default}" + ) + + # set their wallets created_at with the first payment + await db.execute( + """ + UPDATE wallets SET created_at = ( + SELECT time FROM apipayments + WHERE apipayments.wallet = wallets.id + ORDER BY time ASC LIMIT 1 + ) + """ + ) + + # then set their accounts created_at with the wallet + await db.execute( + """ + UPDATE accounts SET created_at = ( + SELECT created_at FROM wallets + WHERE wallets.user = accounts.id + ORDER BY created_at ASC LIMIT 1 + ) + """ + ) + + # set all to now where they are null + now = int(time()) + await db.execute( + f""" + UPDATE wallets SET created_at = {db.timestamp_placeholder} + WHERE created_at IS NULL + """, + (now,), + ) + await db.execute( + f""" + UPDATE accounts SET created_at = {db.timestamp_placeholder} + WHERE created_at IS NULL + """, + (now,), + ) + + except OperationalError as exc: + logger.error(f"Migration 17 failed: {exc}") + pass diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 297dadfc..eef54e30 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -30,6 +30,8 @@ class Wallet(BaseModel): currency: Optional[str] balance_msat: int deleted: bool + created_at: Optional[int] = None + updated_at: Optional[int] = None @property def balance(self) -> int: @@ -98,6 +100,8 @@ class User(BaseModel): super_user: bool = False has_password: bool = False config: Optional[UserConfig] = None + created_at: Optional[int] = None + updated_at: Optional[int] = None @property def wallet_ids(self) -> List[str]: diff --git a/lnbits/db.py b/lnbits/db.py index b9d8e054..be43d849 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -91,6 +91,12 @@ class Compat: return "(strftime('%s', 'now'))" return "" + @property + def timestamp_column_default(self) -> str: + if self.type in {POSTGRES, COCKROACH}: + return self.timestamp_now + return "NULL" + @property def serial_primary_key(self) -> str: if self.type in {POSTGRES, COCKROACH}: