lnbits-legend/lnbits/core/migrations.py

494 lines
14 KiB
Python
Raw Normal View History

2022-12-02 17:39:56 +01:00
import datetime
from time import time
2022-12-06 10:48:34 +01:00
2022-12-06 10:48:16 +01:00
from loguru import logger
from sqlalchemy.exc import OperationalError
2020-09-03 22:39:52 -03:00
2022-10-05 14:17:23 +02:00
from lnbits import bolt11
2020-09-03 22:39:52 -03:00
async def m000_create_migrations_table(db):
await db.execute(
2020-09-03 22:39:52 -03:00
"""
CREATE TABLE IF NOT EXISTS dbversions (
2020-09-03 22:39:52 -03:00
db TEXT PRIMARY KEY,
version INT NOT NULL
)
"""
)
2020-04-16 15:23:38 +02:00
async def m001_initial(db):
2020-04-16 15:23:38 +02:00
"""
Initial LNbits tables.
"""
await db.execute(
2020-04-17 21:13:57 +02:00
"""
CREATE TABLE IF NOT EXISTS accounts (
2020-04-16 15:23:38 +02:00
id TEXT PRIMARY KEY,
email TEXT,
pass TEXT
);
2020-04-17 21:13:57 +02:00
"""
)
await db.execute(
2020-04-17 21:13:57 +02:00
"""
CREATE TABLE IF NOT EXISTS extensions (
2021-06-21 23:22:52 -03:00
"user" TEXT NOT NULL,
2020-04-16 15:23:38 +02:00
extension TEXT NOT NULL,
2021-06-21 23:22:52 -03:00
active BOOLEAN DEFAULT false,
2020-04-16 15:23:38 +02:00
2021-06-21 23:22:52 -03:00
UNIQUE ("user", extension)
2020-04-16 15:23:38 +02:00
);
2020-04-17 21:13:57 +02:00
"""
)
await db.execute(
2020-04-17 21:13:57 +02:00
"""
CREATE TABLE IF NOT EXISTS wallets (
2020-04-16 15:23:38 +02:00
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
2021-06-21 23:22:52 -03:00
"user" TEXT NOT NULL,
2020-04-16 15:23:38 +02:00
adminkey TEXT NOT NULL,
inkey TEXT
);
2020-04-17 21:13:57 +02:00
"""
)
await db.execute(
2021-06-21 23:22:52 -03:00
f"""
CREATE TABLE IF NOT EXISTS apipayments (
2020-04-16 15:23:38 +02:00
payhash TEXT NOT NULL,
amount {db.big_int} NOT NULL,
2020-04-16 15:23:38 +02:00
fee INTEGER NOT NULL DEFAULT 0,
wallet TEXT NOT NULL,
pending BOOLEAN NOT NULL,
memo TEXT,
2021-06-21 23:22:52 -03:00
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
2020-04-16 15:23:38 +02:00
UNIQUE (wallet, payhash)
);
2020-04-17 21:13:57 +02:00
"""
)
2020-08-19 17:53:27 +01:00
await db.execute(
2020-04-17 21:13:57 +02:00
"""
2021-11-15 21:45:13 +00:00
CREATE VIEW balances AS
2020-04-16 15:23:38 +02:00
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
FROM apipayments
2021-06-21 23:22:52 -03:00
WHERE amount > 0 AND pending = false -- don't sum pending
2020-04-16 15:23:38 +02:00
GROUP BY wallet
UNION ALL
SELECT wallet, SUM(amount + fee) AS s -- outgoing, sum fees
FROM apipayments
WHERE amount < 0 -- do sum pending
GROUP BY wallet
2021-06-21 23:22:52 -03:00
)x
2020-04-16 15:23:38 +02:00
GROUP BY wallet;
2020-04-17 21:13:57 +02:00
"""
)
2020-04-16 15:23:38 +02:00
2020-08-19 17:53:27 +01:00
async def m002_add_fields_to_apipayments(db):
2020-08-19 17:53:27 +01:00
"""
Adding fields to apipayments for better accounting,
and renaming payhash to checking_id since that is what it really is.
"""
2020-09-03 22:39:52 -03:00
try:
await db.execute("ALTER TABLE apipayments RENAME COLUMN payhash TO checking_id")
await db.execute("ALTER TABLE apipayments ADD COLUMN hash TEXT")
await db.execute("CREATE INDEX by_hash ON apipayments (hash)")
await db.execute("ALTER TABLE apipayments ADD COLUMN preimage TEXT")
await db.execute("ALTER TABLE apipayments ADD COLUMN bolt11 TEXT")
await db.execute("ALTER TABLE apipayments ADD COLUMN extra TEXT")
2020-09-03 22:39:52 -03:00
import json
rows = await (await db.execute("SELECT * FROM apipayments")).fetchall()
2020-09-03 22:39:52 -03:00
for row in rows:
if not row["memo"] or not row["memo"].startswith("#"):
continue
2020-04-16 15:23:38 +02:00
2020-09-03 22:39:52 -03:00
for ext in ["withdraw", "events", "lnticket", "paywall", "tpos"]:
prefix = f"#{ext} "
2020-09-03 22:39:52 -03:00
if row["memo"].startswith(prefix):
new = row["memo"][len(prefix) :]
await db.execute(
2020-09-03 22:39:52 -03:00
"""
UPDATE apipayments SET extra = ?, memo = ?
WHERE checking_id = ? AND memo = ?
""",
(
json.dumps({"tag": ext}),
new,
row["checking_id"],
row["memo"],
),
2020-09-03 22:39:52 -03:00
)
break
except OperationalError:
2020-09-03 22:39:52 -03:00
# this is necessary now because it may be the case that this migration will
# run twice in some environments.
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
async def m003_add_invoice_webhook(db):
"""
Special column for webhook endpoints that can be assigned
to each different invoice.
"""
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook TEXT")
await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT")
async def m004_ensure_fees_are_always_negative(db):
"""
Use abs() so wallet backends don't have to care about the sign of the fees.
"""
await db.execute("DROP VIEW balances")
await db.execute(
"""
2021-11-15 21:45:13 +00:00
CREATE VIEW balances AS
SELECT wallet, COALESCE(SUM(s), 0) AS balance FROM (
SELECT wallet, SUM(amount) AS s -- incoming
FROM apipayments
2021-06-21 23:22:52 -03:00
WHERE amount > 0 AND pending = false -- don't sum pending
GROUP BY wallet
UNION ALL
SELECT wallet, SUM(amount - abs(fee)) AS s -- outgoing, sum fees
FROM apipayments
WHERE amount < 0 -- do sum pending
GROUP BY wallet
2021-06-21 23:22:52 -03:00
)x
GROUP BY wallet;
"""
)
2021-04-17 18:27:15 -03:00
async def m005_balance_check_balance_notify(db):
"""
2023-04-17 08:29:01 +02:00
Keep track of balanceCheck-enabled lnurl-withdrawals to be consumed by an
LNbits wallet and of balanceNotify URLs supplied by users to empty their wallets.
2021-04-17 18:27:15 -03:00
"""
await db.execute(
"""
CREATE TABLE IF NOT EXISTS balance_check (
2021-06-21 23:22:52 -03:00
wallet TEXT NOT NULL REFERENCES wallets (id),
2021-04-17 18:27:15 -03:00
service TEXT NOT NULL,
url TEXT NOT NULL,
UNIQUE(wallet, service)
);
"""
)
await db.execute(
"""
CREATE TABLE IF NOT EXISTS balance_notify (
2021-06-21 23:22:52 -03:00
wallet TEXT NOT NULL REFERENCES wallets (id),
2021-04-17 18:27:15 -03:00
url TEXT NOT NULL,
UNIQUE(wallet, url)
);
"""
)
2022-10-05 14:17:23 +02:00
async def m006_add_invoice_expiry_to_apipayments(db):
"""
2022-12-06 16:21:19 +01:00
Adds invoice expiry column to apipayments.
2022-10-05 14:17:23 +02:00
"""
2022-12-02 17:38:36 +01:00
try:
await db.execute("ALTER TABLE apipayments ADD COLUMN expiry TIMESTAMP")
except OperationalError:
pass
2022-12-06 13:23:51 +01:00
2022-12-06 16:21:19 +01:00
async def m007_set_invoice_expiries(db):
"""
Precomputes invoice expiry for existing pending incoming payments.
"""
2022-10-05 14:17:23 +02:00
try:
rows = await (
await db.execute(
f"""
SELECT bolt11, checking_id
FROM apipayments
WHERE pending = true
2022-12-06 21:04:10 +01:00
AND amount > 0
2022-10-05 14:17:23 +02:00
AND bolt11 IS NOT NULL
AND expiry IS NULL
2022-12-06 21:04:10 +01:00
AND time < {db.timestamp_now}
2022-10-05 14:17:23 +02:00
"""
)
).fetchall()
2022-12-06 21:05:09 +01:00
if len(rows):
2022-12-28 16:06:01 -08:00
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
2022-10-05 14:17:23 +02:00
for i, (
payment_request,
2022-12-06 10:48:16 +01:00
checking_id,
2022-10-05 14:17:23 +02:00
) in enumerate(rows):
try:
invoice = bolt11.decode(payment_request)
2022-12-06 10:48:16 +01:00
if invoice.expiry is None:
continue
expiration_date = datetime.datetime.fromtimestamp(
invoice.date + invoice.expiry
)
logger.info(
f"Migration: {i+1}/{len(rows)} setting expiry of invoice"
f" {invoice.payment_hash} to {expiration_date}"
2022-12-06 10:48:16 +01:00
)
await db.execute(
"""
UPDATE apipayments SET expiry = ?
WHERE checking_id = ? AND amount > 0
""",
(
db.datetime_to_timestamp(expiration_date),
checking_id,
),
)
except Exception:
2022-10-05 14:17:23 +02:00
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
# run twice in some environments.
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
2022-12-12 10:49:31 +02:00
2022-12-12 10:52:01 +02:00
2022-12-12 10:49:31 +02:00
async def m008_create_admin_settings_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS settings (
super_user TEXT,
2022-12-09 13:14:22 +02:00
editable_settings TEXT NOT NULL DEFAULT '{}'
);
"""
)
2023-01-25 09:56:05 +02:00
2023-01-12 20:25:51 +00:00
async def m009_create_tinyurl_table(db):
2023-01-12 15:16:37 +00:00
await db.execute(
f"""
2023-01-12 20:37:03 +00:00
CREATE TABLE IF NOT EXISTS tiny_url (
2023-01-12 15:16:37 +00:00
id TEXT PRIMARY KEY,
url TEXT,
endless BOOL NOT NULL DEFAULT false,
wallet TEXT,
2023-01-24 13:27:30 +00:00
time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
2023-01-12 15:16:37 +00:00
);
"""
)
2023-01-25 09:56:05 +02:00
async def m010_create_installed_extensions_table(db):
await db.execute(
"""
CREATE TABLE IF NOT EXISTS installed_extensions (
id TEXT PRIMARY KEY,
version TEXT NOT NULL,
name TEXT NOT NULL,
2023-01-17 16:28:24 +02:00
short_description TEXT,
icon TEXT,
stars INT NOT NULL DEFAULT 0,
active BOOLEAN DEFAULT false,
meta TEXT NOT NULL DEFAULT '{}'
);
"""
)
async def m011_optimize_balances_view(db):
"""
Make the calculation of the balance a single aggregation
over the payments table instead of 2.
"""
await db.execute("DROP VIEW balances")
await db.execute(
"""
CREATE VIEW balances AS
SELECT wallet, SUM(amount - abs(fee)) AS balance
FROM apipayments
WHERE (pending = false AND amount > 0) OR amount < 0
GROUP BY wallet
"""
)
async def m012_add_currency_to_wallet(db):
await db.execute(
"""
ALTER TABLE wallets ADD COLUMN currency TEXT
"""
)
async def m013_add_deleted_to_wallets(db):
"""
Adds deleted column to wallets.
"""
try:
await db.execute(
"ALTER TABLE wallets ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT false"
)
except OperationalError:
pass
async def m014_set_deleted_wallets(db):
"""
Sets deleted column to wallets.
"""
try:
rows = await (
await db.execute(
"""
SELECT *
FROM wallets
WHERE user LIKE 'del:%'
AND adminkey LIKE 'del:%'
AND inkey LIKE 'del:%'
"""
)
).fetchall()
for row in rows:
try:
user = row[2].split(":")[1]
adminkey = row[3].split(":")[1]
inkey = row[4].split(":")[1]
await db.execute(
"""
UPDATE wallets SET user = ?, adminkey = ?, inkey = ?, deleted = true
WHERE id = ?
""",
(user, adminkey, inkey, row[0]),
)
except Exception:
continue
except OperationalError:
# this is necessary now because it may be the case that this migration will
# run twice in some environments.
# catching errors like this won't be necessary in anymore now that we
# keep track of db versions so no migration ever runs twice.
pass
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
async def m015_create_push_notification_subscriptions_table(db):
await db.execute(
f"""
CREATE TABLE IF NOT EXISTS webpush_subscriptions (
endpoint TEXT NOT NULL,
"user" TEXT NOT NULL,
data TEXT NOT NULL,
host TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
PRIMARY KEY (endpoint, "user")
);
"""
)
[FEAT] Auth, Login, OAuth, create account with username and password #1653 (#2092) no more superuser url! delete cookie on logout add usr login feature fix node management * Cleaned up login form * CreateUser * information leak * cleaner parsing usr from url * rename decorators * login secret * fix: add back `superuser` command * chore: remove `fastapi_login` * fix: extract `token` from cookie * chore: prepare to extract user * feat: check user * chore: code clean-up * feat: happy flow working * fix: usr only login * fix: user already logged in * feat: check user in URL * fix: verify password at DB level * fix: do not show `Login` controls if user already logged in * fix: separate login endpoints * fix: remove `usr` param * chore: update error message * refactor: register method * feat: logout * chore: move comments * fix: remove user auth check from API * fix: user check unnecessary * fix: redirect after logout * chore: remove garbage files * refactor: simplify constructor call * fix: hide user icon if not authorized * refactor: rename auth env vars * chore: code clean-up * fix: add types for `python-jose` * fix: add types for `passlib` * fix: return type * feat: set default value for `auth_secret_key` to hash of super user * fix: default value * feat: rework login page * feat: ui polishing * feat: google auth * feat: add google auth * chore: remove `authlib` dependency * refactor: extract `_handle_sso_login` method * refactor: convert methods to `properties` * refactor: rename: `user_api` to `auth_api` * feat: store user info from SSO * chore: re-arange the buttons * feat: conditional rendering of login options * feat: correctly render buttons * fix: re-add `Claim Bitcoin` from the main page * fix: create wallet must send new user * fix: no `username-password` auth method * refactor: rename auth method * fix: do not force API level UUID4 validation * feat: add validation for username * feat: add account page * feat: update account * feat: add `has_password` for user * fix: email not editable * feat: validate email for existing account * fix: register check * feat: reset password * chore: code clean-up * feat: handle token expired * fix: only redirect if `text/html` * refactor: remove `OAuth2PasswordRequestForm` * chore: remove `python-multipart` dependency * fix: handle no headers for exception * feat: add back button on error screen * feat: show user profile image * fix: check account creation permissions * fix: auth for internal api call * chore: add some docs * chore: code clean-up * fix: rebase stuff * fix: default value types * refactor: customize error messages * fix: move types libs to dev dependencies * doc: specify the `Authorization callback URL` * fix: pass missing superuser id in node ui test * fix: keep usr param on wallet redirect removing usr param causes an issue if the browser doesnt yet have an access token. * fix: do not redirect if `wal` query param not present * fix: add nativeBuildInputs and buildInputs overrides to flake.nix * bump fastapi-sso to 0.9.0 which fixes some security issues * refactor: move the `lnbits_admin_extensions` to decorators * chore: bring package config from `dev` * chore: re-add dependencies * chore: re-add cev dependencies * chore: re-add mypy ignores * feat: i18n * refactor: move admin ext check to decorator (fix after rebase) * fix: label mapping * fix: re-fetch user after first wallet was created * fix: unlikely case that `user` is not found * refactor translations (move '*' to code) * reorganize deps in pyproject.toml, add comment * update flake.lock and simplify flake.nix after upstreaming overrides for fastapi-sso, types-passlib, types-pyasn1, types-python-jose were upstreamed in https://github.com/nix-community/poetry2nix/pull/1463 * fix: more relaxed email verification (by @prusnak) * fix: remove `\b` (boundaries) since we re using `fullmatch` * chore: `make bundle` --------- Co-authored-by: dni ⚡ <office@dnilabs.com> Co-authored-by: Arc <ben@arc.wales> Co-authored-by: jackstar12 <jkranawetter05@gmail.com> Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
2023-12-12 12:38:19 +02:00
async def m016_add_username_column_to_accounts(db):
"""
Adds username column to accounts.
"""
try:
await db.execute("ALTER TABLE accounts ADD COLUMN username TEXT")
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
async def m018_balances_view_exclude_deleted(db):
"""
Make deleted wallets not show up in the balances view.
"""
await db.execute("DROP VIEW balances")
await db.execute(
"""
CREATE VIEW balances AS
SELECT apipayments.wallet,
SUM(apipayments.amount - ABS(apipayments.fee)) AS balance
FROM apipayments
LEFT JOIN wallets ON apipayments.wallet = wallets.id
WHERE (wallets.deleted = false OR wallets.deleted is NULL)
AND ((apipayments.pending = false AND apipayments.amount > 0)
OR apipayments.amount < 0)
GROUP BY wallet
"""
)