From aaa3b1d4d7f4e5baa082fe52318fbaff6165f421 Mon Sep 17 00:00:00 2001 From: dni Date: Tue, 2 Jun 2026 10:06:00 +0200 Subject: [PATCH 1/3] fix: split create and update endpoints and fix update wallet check issue were you could put a wallet id into and update where the wallet didnt belong to you. --- config.json | 2 +- views_api.py | 170 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 106 insertions(+), 66 deletions(-) diff --git a/config.json b/config.json index 5742544..0470825 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "paylink", - "version": "1.3.0", + "version": "1.3.1", "name": "Pay Links", "repo": "https://github.com/lnbits/lnurlp", "short_description": "Make static reusable LNURL pay links or lightning addresses", diff --git a/views_api.py b/views_api.py index eac8a97..743adaf 100644 --- a/views_api.py +++ b/views_api.py @@ -47,6 +47,54 @@ def check_lnurl_encode(req: Request, link: PayLink) -> str: ) from exc +def validate_paylink(data: CreatePayLinkData): + if data.min > data.max: + raise HTTPException( + detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST + ) + + if not data.currency: + if round(data.min) != data.min or round(data.max) != data.max or data.min < 1: + raise HTTPException( + detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST + ) + + if data.webhook_headers: + try: + json.loads(data.webhook_headers) + except ValueError as exc: + raise HTTPException( + detail="Invalid JSON in webhook_headers.", + status_code=HTTPStatus.BAD_REQUEST, + ) from exc + + if data.webhook_body: + try: + json.loads(data.webhook_body) + except ValueError as exc: + raise HTTPException( + detail="Invalid JSON in webhook_body.", + status_code=HTTPStatus.BAD_REQUEST, + ) from exc + + if data.username and not re.match("^[a-z0-9-_.]{1,210}$", data.username): + raise HTTPException( + detail=f"Invalid username: {data.username}. " + "Only letters a-z0-9-_. allowed, min 1 and max 210 characters!", + status_code=HTTPStatus.BAD_REQUEST, + ) + + if ( + data.success_url + and data.success_url != "" + and not data.success_url.startswith("https://") + ): + raise HTTPException( + detail="Success URL must be secure https://...", + status_code=HTTPStatus.BAD_REQUEST, + ) + + @lnurlp_api_router.get("/api/v1/links", status_code=HTTPStatus.OK) async def api_links( req: Request, @@ -110,41 +158,13 @@ async def check_username_exists(username: str): @lnurlp_api_router.post("/api/v1/links", status_code=HTTPStatus.CREATED) -@lnurlp_api_router.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) -async def api_link_create_or_update( +async def api_link_create( req: Request, data: CreatePayLinkData, - link_id: str | None = None, key_info: WalletTypeInfo = Depends(require_admin_key), ) -> PayLink: - if data.min > data.max: - raise HTTPException( - detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST - ) - - if not data.currency: - if round(data.min) != data.min or round(data.max) != data.max or data.min < 1: - raise HTTPException( - detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST - ) - - if data.webhook_headers: - try: - json.loads(data.webhook_headers) - except ValueError as exc: - raise HTTPException( - detail="Invalid JSON in webhook_headers.", - status_code=HTTPStatus.BAD_REQUEST, - ) from exc - if data.webhook_body: - try: - json.loads(data.webhook_body) - except ValueError as exc: - raise HTTPException( - detail="Invalid JSON in webhook_body.", - status_code=HTTPStatus.BAD_REQUEST, - ) from exc + validate_paylink(data) # database only allows int4 entries for min and max. For fiat currencies, # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents. @@ -152,23 +172,58 @@ async def api_link_create_or_update( data.min *= data.fiat_base_multiplier data.max *= data.fiat_base_multiplier - if ( - data.success_url - and data.success_url != "" - and not data.success_url.startswith("https://") - ): + # if wallet is not provided, use the wallet of the key + if not data.wallet: + data.wallet = key_info.wallet.id + + new_wallet = await get_wallet(data.wallet) + if not new_wallet: raise HTTPException( - detail="Success URL must be secure https://...", - status_code=HTTPStatus.BAD_REQUEST, + detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN ) + user = await get_user(key_info.wallet.user) + if not user: + raise HTTPException( + detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN + ) + if new_wallet.id not in user.wallet_ids: + raise HTTPException(detail="Not your wallet.", status_code=HTTPStatus.FORBIDDEN) - if data.username and not re.match("^[a-z0-9-_.]{1,210}$", data.username): + if data.username: + await check_username_exists(data.username) + + link = await create_pay_link(data) + link.lnurl = check_lnurl_encode(req, link) + return link + + +@lnurlp_api_router.put("/api/v1/links/{link_id}") +async def api_link_update( + req: Request, + data: CreatePayLinkData, + link_id: str, + key_info: WalletTypeInfo = Depends(require_admin_key), +) -> PayLink: + + validate_paylink(data) + + link = await get_pay_link(link_id) + if not link: raise HTTPException( - detail=f"Invalid username: {data.username}. " - "Only letters a-z0-9-_. allowed, min 1 and max 210 characters!", - status_code=HTTPStatus.BAD_REQUEST, + detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND + ) + + if link.wallet != key_info.wallet.id: + raise HTTPException( + detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) + # database only allows int4 entries for min and max. For fiat currencies, + # we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents. + if data.currency and data.fiat_base_multiplier: + data.min *= data.fiat_base_multiplier + data.max *= data.fiat_base_multiplier + # if wallet is not provided, use the wallet of the key if not data.wallet: data.wallet = key_info.wallet.id @@ -178,36 +233,21 @@ async def api_link_create_or_update( raise HTTPException( detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN ) - - # admins are allowed to create/edit paylinks belonging to regular users user = await get_user(key_info.wallet.user) - admin_user = user.admin if user else False - if not admin_user and new_wallet.user != key_info.wallet.user: + if not user: raise HTTPException( - detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN + detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN ) + if new_wallet.id not in user.wallet_ids: + raise HTTPException(detail="Not your wallet.", status_code=HTTPStatus.FORBIDDEN) - if link_id: - link = await get_pay_link(link_id) - - if not link: - raise HTTPException( - detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND - ) - - if data.username and data.username != link.username: - await check_username_exists(data.username) - - for k, v in data.dict().items(): - setattr(link, k, v) - - link = await update_pay_link(link) - else: - if data.username: - await check_username_exists(data.username) + if data.username and data.username != link.username: + await check_username_exists(data.username) - link = await create_pay_link(data) + for k, v in data.dict().items(): + setattr(link, k, v) + link = await update_pay_link(link) link.lnurl = check_lnurl_encode(req, link) return link From acfd22db43a0a10c400eca2dba660ae219588104 Mon Sep 17 00:00:00 2001 From: dni Date: Tue, 2 Jun 2026 10:24:34 +0200 Subject: [PATCH 2/3] fixup! --- views_api.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/views_api.py b/views_api.py index 743adaf..6e47c05 100644 --- a/views_api.py +++ b/views_api.py @@ -213,7 +213,13 @@ async def api_link_update( detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND ) - if link.wallet != key_info.wallet.id: + user = await get_user(key_info.wallet.user) + if not user: + raise HTTPException( + detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN + ) + + if link.wallet not in user.wallet_ids: raise HTTPException( detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) @@ -233,11 +239,6 @@ async def api_link_update( raise HTTPException( detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN ) - user = await get_user(key_info.wallet.user) - if not user: - raise HTTPException( - detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN - ) if new_wallet.id not in user.wallet_ids: raise HTTPException(detail="Not your wallet.", status_code=HTTPStatus.FORBIDDEN) From 0ae53e621f28d07c271e3303f8727aa287a7ac0a Mon Sep 17 00:00:00 2001 From: dni Date: Tue, 2 Jun 2026 10:46:51 +0200 Subject: [PATCH 3/3] admin exception --- views_api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/views_api.py b/views_api.py index 6e47c05..5e4767a 100644 --- a/views_api.py +++ b/views_api.py @@ -186,7 +186,8 @@ async def api_link_create( raise HTTPException( detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN ) - if new_wallet.id not in user.wallet_ids: + + if not user.admin and new_wallet.id not in user.wallet_ids: raise HTTPException(detail="Not your wallet.", status_code=HTTPStatus.FORBIDDEN) if data.username: @@ -219,7 +220,7 @@ async def api_link_update( detail="User does not exist.", status_code=HTTPStatus.FORBIDDEN ) - if link.wallet not in user.wallet_ids: + if not user.admin and link.wallet not in user.wallet_ids: raise HTTPException( detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) @@ -239,7 +240,7 @@ async def api_link_update( raise HTTPException( detail="Wallet does not exist.", status_code=HTTPStatus.FORBIDDEN ) - if new_wallet.id not in user.wallet_ids: + if not user.admin and new_wallet.id not in user.wallet_ids: raise HTTPException(detail="Not your wallet.", status_code=HTTPStatus.FORBIDDEN) if data.username and data.username != link.username: