lnbits-legend/lnbits/core/migrations.py

523 lines
15 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
"""
)
async def m019_balances_view_based_on_wallets(db):
"""
Make deleted wallets not show up in the balances view.
Important for querying whole lnbits balances.
"""
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 wallets
LEFT JOIN apipayments 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 apipayments.wallet
"""
)
[feat] Pay to enable extension (#2516) * feat: add payment tab * feat: add buttons * feat: persist `pay to enable` changes * fix: do not disable extension on upgrade * fix: show releases tab first * feat: extract `enableExtension` logic * refactor: rename routes * feat: show dialog for paying extension * feat: create invoice to enable * refactor: extract enable/disable extension logic * feat: add extra info to UserExtensions * feat: check payment for extension enable * fix: parsing * feat: admins must not pay * fix: code checks * fix: test * refactor: extract extension activate/deactivate to the `api` side * feat: add `get_user_extensions ` * feat: return explicit `requiresPayment` * feat: add `isPaymentRequired` to extension list * fix: `paid_to_enable` status * fix: ui layout * feat: show QR Code * feat: wait for invoice to be paid * test: removed deprecated test and dead code * feat: add re-check button * refactor: rename paths for endpoints * feat: i18n * feat: add `{"success": True}` * test: fix listener * fix: rebase errors * chore: update bundle * fix: return error status code for the HTML error pages * fix: active extension loading from file system * chore: temp commit * fix: premature optimisation * chore: make check * refactor: remove extracted logic * chore: code format * fix: enable by default after install * fix: use `discard` instead of `remove` for `set` * chore: code format * fix: better error code * fix: check for stop function before invoking * feat: check if the wallet belongs to the admin user * refactor: return 402 Requires Payment * chore: more typing * chore: temp checkout different branch for tests * fix: too much typing * fix: remove try-except * fix: typo * fix: manual format * fix: merge issue * remove this line --------- Co-authored-by: dni ⚡ <office@dnilabs.com>
2024-05-28 14:07:33 +03:00
async def m020_add_column_column_to_user_extensions(db):
"""
Adds extra column to user extensions.
"""
await db.execute("ALTER TABLE extensions ADD COLUMN extra TEXT")