feat: update to lnbits 1.0.0 (#41)
--------- Co-authored-by: Pavol Rusnak <pavol@rusnak.io>
This commit is contained in:
parent
239bfb65ab
commit
7f8b269ecd
12 changed files with 1513 additions and 1457 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Bolt Cards",
|
||||
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
||||
"tile": "/boltcards/static/image/boltcard.png",
|
||||
"min_lnbits_version": "0.12.5",
|
||||
"min_lnbits_version": "1.0.0",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "dni",
|
||||
|
|
|
|||
230
crud.py
230
crud.py
|
|
@ -1,6 +1,6 @@
|
|||
import secrets
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
|
@ -31,175 +31,157 @@ async def create_card(data: CreateCardData, wallet_id: str) -> Card:
|
|||
k2,
|
||||
otp
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (
|
||||
:id, :uid, :external_id, :wallet, :card_name, :counter,
|
||||
:tx_limit, :daily_limit, :enable, :k0, :k1, :k2, :otp
|
||||
)
|
||||
""",
|
||||
(
|
||||
card_id,
|
||||
data.uid.upper(),
|
||||
extenal_id,
|
||||
wallet_id,
|
||||
data.card_name,
|
||||
data.counter,
|
||||
data.tx_limit,
|
||||
data.daily_limit,
|
||||
True,
|
||||
data.k0,
|
||||
data.k1,
|
||||
data.k2,
|
||||
secrets.token_hex(16),
|
||||
),
|
||||
{
|
||||
"id": card_id,
|
||||
"uid": data.uid.upper(),
|
||||
"external_id": extenal_id,
|
||||
"wallet": wallet_id,
|
||||
"card_name": data.card_name,
|
||||
"counter": data.counter,
|
||||
"tx_limit": data.tx_limit,
|
||||
"daily_limit": data.daily_limit,
|
||||
"enable": True,
|
||||
"k0": data.k0,
|
||||
"k1": data.k1,
|
||||
"k2": data.k2,
|
||||
"otp": secrets.token_hex(16),
|
||||
},
|
||||
)
|
||||
card = await get_card(card_id)
|
||||
assert card, "Newly created card couldn't be retrieved"
|
||||
return card
|
||||
|
||||
|
||||
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
|
||||
if "is_unique" in kwargs:
|
||||
kwargs["is_unique"] = int(kwargs["is_unique"])
|
||||
if "uid" in kwargs:
|
||||
kwargs["uid"] = kwargs["uid"].upper()
|
||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
||||
await db.execute(
|
||||
f"UPDATE boltcards.cards SET {q} WHERE id = ?",
|
||||
(*kwargs.values(), card_id),
|
||||
async def update_card(card_id: str, data: CreateCardData) -> Card:
|
||||
card = Card(
|
||||
id=card_id,
|
||||
**data.dict(),
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||
return Card(**row) if row else None
|
||||
await db.update("boltcards.cards", card)
|
||||
return card
|
||||
|
||||
|
||||
async def get_cards(wallet_ids: List[str]) -> List[Card]:
|
||||
async def get_cards(wallet_ids: list[str]) -> list[Card]:
|
||||
if len(wallet_ids) == 0:
|
||||
return []
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||
return await db.fetchall(
|
||||
f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})",
|
||||
model=Card,
|
||||
)
|
||||
|
||||
return [Card(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_card(card_id: str) -> Optional[Card]:
|
||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||
if not row:
|
||||
return None
|
||||
|
||||
card = dict(**row)
|
||||
|
||||
return Card.parse_obj(card)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE id = :id",
|
||||
{"id": card_id},
|
||||
Card,
|
||||
)
|
||||
|
||||
|
||||
async def get_card_by_uid(card_uid: str) -> Optional[Card]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE uid = ?", (card_uid.upper(),)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE uid = :uid",
|
||||
{"uid": card_uid.upper()},
|
||||
Card,
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
card = dict(**row)
|
||||
|
||||
return Card.parse_obj(card)
|
||||
|
||||
|
||||
async def get_card_by_external_id(external_id: str) -> Optional[Card]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE external_id = ?", (external_id.lower(),)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE external_id = :ext_id",
|
||||
{"ext_id": external_id.lower()},
|
||||
Card,
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
card = dict(**row)
|
||||
|
||||
return Card.parse_obj(card)
|
||||
|
||||
|
||||
async def get_card_by_otp(otp: str) -> Optional[Card]:
|
||||
row = await db.fetchone("SELECT * FROM boltcards.cards WHERE otp = ?", (otp,))
|
||||
if not row:
|
||||
return None
|
||||
|
||||
card = dict(**row)
|
||||
|
||||
return Card.parse_obj(card)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.cards WHERE otp = :otp",
|
||||
{"otp": otp},
|
||||
Card,
|
||||
)
|
||||
|
||||
|
||||
async def delete_card(card_id: str) -> None:
|
||||
# Delete cards
|
||||
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||
await db.execute("DELETE FROM boltcards.cards WHERE id = :id", {"id": card_id})
|
||||
# Delete hits
|
||||
hits = await get_hits([card_id])
|
||||
for hit in hits:
|
||||
await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
|
||||
await db.execute("DELETE FROM boltcards.hits WHERE id = :id", {"id": hit.id})
|
||||
# Delete refunds
|
||||
refunds = await get_refunds([hit.id])
|
||||
for refund in refunds:
|
||||
await db.execute(
|
||||
"DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
|
||||
"DELETE FROM boltcards.refunds WHERE id = :id", {"id": refund.id}
|
||||
)
|
||||
|
||||
|
||||
async def update_card_counter(counter: int, card_id: str):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
|
||||
(counter, card_id),
|
||||
"UPDATE boltcards.cards SET counter = :counter WHERE id = :id",
|
||||
{"counter": counter, "id": card_id},
|
||||
)
|
||||
|
||||
|
||||
async def enable_disable_card(enable: bool, card_id: str) -> Optional[Card]:
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET enable = ? WHERE id = ?",
|
||||
(enable, card_id),
|
||||
"UPDATE boltcards.cards SET enable = :enable WHERE id = :id",
|
||||
{"enable": enable, "id": card_id},
|
||||
)
|
||||
return await get_card(card_id)
|
||||
|
||||
|
||||
async def update_card_otp(otp: str, card_id: str):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.cards SET otp = ? WHERE id = ?",
|
||||
(otp, card_id),
|
||||
"UPDATE boltcards.cards SET otp = :otp WHERE id = :id",
|
||||
{"otp": otp, "id": card_id},
|
||||
)
|
||||
|
||||
|
||||
async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||
row = await db.fetchone("SELECT * FROM boltcards.hits WHERE id = ?", (hit_id,))
|
||||
if not row:
|
||||
return None
|
||||
|
||||
hit = dict(**row)
|
||||
|
||||
return Hit.parse_obj(hit)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.hits WHERE id = :id",
|
||||
{"id": hit_id},
|
||||
Hit,
|
||||
)
|
||||
|
||||
|
||||
async def get_hits(cards_ids: List[str]) -> List[Hit]:
|
||||
async def get_hits(cards_ids: list[str]) -> list[Hit]:
|
||||
if len(cards_ids) == 0:
|
||||
return []
|
||||
|
||||
q = ",".join(["?"] * len(cards_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
|
||||
q = ",".join([f"'{card_id}'" for card_id in cards_ids])
|
||||
return await db.fetchall(
|
||||
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})",
|
||||
model=Hit,
|
||||
)
|
||||
|
||||
return [Hit(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_hits_today(card_id: str) -> List[Hit]:
|
||||
async def get_hits_today(card_id: str) -> list[Hit]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM boltcards.hits WHERE card_id = ?",
|
||||
(card_id,),
|
||||
"SELECT * FROM boltcards.hits WHERE card_id = :id",
|
||||
{"id": card_id},
|
||||
Hit,
|
||||
)
|
||||
updatedrow = []
|
||||
for row in rows:
|
||||
if datetime.now().date() == datetime.fromtimestamp(row.time).date():
|
||||
updatedrow.append(row)
|
||||
for hit in rows:
|
||||
if datetime.now().date() == hit.time.date():
|
||||
updatedrow.append(hit)
|
||||
|
||||
return [Hit(**row) for row in updatedrow]
|
||||
return updatedrow
|
||||
|
||||
|
||||
async def spend_hit(card_id: str, amount: int):
|
||||
await db.execute(
|
||||
"UPDATE boltcards.hits SET spent = ?, amount = ? WHERE id = ?",
|
||||
(True, amount, card_id),
|
||||
"UPDATE boltcards.hits SET spent = :spent, amount = :amount WHERE id = :id",
|
||||
{"spent": True, "amount": amount, "id": card_id},
|
||||
)
|
||||
return await get_hit(card_id)
|
||||
|
||||
|
|
@ -218,18 +200,18 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
|
|||
new_ctr,
|
||||
amount
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (:id, :card_id, :ip, :spent, :useragent, :old_ctr, :new_ctr, :amount)
|
||||
""",
|
||||
(
|
||||
hit_id,
|
||||
card_id,
|
||||
ip,
|
||||
False,
|
||||
useragent,
|
||||
old_ctr,
|
||||
new_ctr,
|
||||
0,
|
||||
),
|
||||
{
|
||||
"id": hit_id,
|
||||
"card_id": card_id,
|
||||
"ip": ip,
|
||||
"spent": False,
|
||||
"useragent": useragent,
|
||||
"old_ctr": old_ctr,
|
||||
"new_ctr": new_ctr,
|
||||
"amount": 0,
|
||||
},
|
||||
)
|
||||
hit = await get_hit(hit_id)
|
||||
assert hit, "Newly recorded hit couldn't be retrieved"
|
||||
|
|
@ -245,13 +227,13 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
|||
hit_id,
|
||||
refund_amount
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
VALUES (:id, :hit_id, :refund_amount)
|
||||
""",
|
||||
(
|
||||
refund_id,
|
||||
hit_id,
|
||||
refund_amount,
|
||||
),
|
||||
{
|
||||
"id": refund_id,
|
||||
"hit_id": hit_id,
|
||||
"refund_amount": refund_amount,
|
||||
},
|
||||
)
|
||||
refund = await get_refund(refund_id)
|
||||
assert refund, "Newly recorded hit couldn't be retrieved"
|
||||
|
|
@ -259,22 +241,18 @@ async def create_refund(hit_id, refund_amount) -> Refund:
|
|||
|
||||
|
||||
async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM boltcards.refunds WHERE id = ?", (refund_id,)
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM boltcards.refunds WHERE id = :id",
|
||||
{"id": refund_id},
|
||||
Refund,
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
refund = dict(**row)
|
||||
return Refund.parse_obj(refund)
|
||||
|
||||
|
||||
async def get_refunds(hits_ids: List[str]) -> List[Refund]:
|
||||
async def get_refunds(hits_ids: list[str]) -> list[Refund]:
|
||||
if len(hits_ids) == 0:
|
||||
return []
|
||||
|
||||
q = ",".join(["?"] * len(hits_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
|
||||
q = ",".join([f"'{hit_id}'" for hit_id in hits_ids])
|
||||
return await db.fetchall(
|
||||
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})",
|
||||
model=Refund,
|
||||
)
|
||||
|
||||
return [Refund(**row) for row in rows]
|
||||
|
|
|
|||
26
models.py
26
models.py
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from sqlite3 import Row
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Query, Request
|
||||
from lnurl import Lnurl
|
||||
|
|
@ -17,8 +17,10 @@ class Card(BaseModel):
|
|||
uid: str
|
||||
external_id: str
|
||||
counter: int
|
||||
tx_limit: int
|
||||
daily_limit: int
|
||||
# TODO: database column is TEXT should be INT
|
||||
tx_limit: str
|
||||
# TODO: database column is TEXT should be INT
|
||||
daily_limit: str
|
||||
enable: bool
|
||||
k0: str
|
||||
k1: str
|
||||
|
|
@ -27,11 +29,7 @@ class Card(BaseModel):
|
|||
prev_k1: str
|
||||
prev_k2: str
|
||||
otp: str
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Card":
|
||||
return cls(**dict(row))
|
||||
time: datetime
|
||||
|
||||
def lnurl(self, req: Request) -> Lnurl:
|
||||
url = str(
|
||||
|
|
@ -67,19 +65,11 @@ class Hit(BaseModel):
|
|||
old_ctr: int
|
||||
new_ctr: int
|
||||
amount: int
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Hit":
|
||||
return cls(**dict(row))
|
||||
time: datetime
|
||||
|
||||
|
||||
class Refund(BaseModel):
|
||||
id: str
|
||||
hit_id: str
|
||||
refund_amount: int
|
||||
time: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Refund":
|
||||
return cls(**dict(row))
|
||||
time: datetime
|
||||
|
|
|
|||
2236
poetry.lock
generated
2236
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10 | ^3.9"
|
||||
lnbits = "*"
|
||||
lnbits = {version = "*", allow-prereleases = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.3.0"
|
||||
|
|
@ -14,7 +14,7 @@ pytest-asyncio = "^0.21.0"
|
|||
pytest = "^7.3.2"
|
||||
mypy = "^1.5.1"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.3.2"
|
||||
ruff = "^0.6.3"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
const mapCards = obj => {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
|
||||
obj.date = Quasar.date.formatDate(new Date(obj.time), 'YYYY-MM-DD HH:mm')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
toggleAdvanced: false,
|
||||
nfcTagReading: false,
|
||||
disableNfcButton: true,
|
||||
lnurlLink: `${window.location.host}/boltcards/api/v1/scan/`,
|
||||
cards: [],
|
||||
hits: [],
|
||||
|
|
@ -155,107 +149,77 @@ new Vue({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
readNfcTag: function () {
|
||||
try {
|
||||
const self = this
|
||||
|
||||
if (typeof NDEFReader == 'undefined') {
|
||||
throw {
|
||||
toString: function () {
|
||||
return 'NFC not supported on this device or browser.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readNfcTag() {
|
||||
const ndef = new NDEFReader()
|
||||
|
||||
const readerAbortController = new AbortController()
|
||||
readerAbortController.signal.onabort = event => {
|
||||
console.log('All NFC Read operations have been aborted.')
|
||||
}
|
||||
|
||||
this.nfcTagReading = true
|
||||
this.$q.notify({
|
||||
Quasar.Notify.create({
|
||||
message: 'Tap your NFC tag to copy its UID here.'
|
||||
})
|
||||
|
||||
return ndef.scan({signal: readerAbortController.signal}).then(() => {
|
||||
ndef.onreadingerror = () => {
|
||||
self.nfcTagReading = false
|
||||
|
||||
this.$q.notify({
|
||||
this.disableNfcButton = false
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: 'There was an error reading this NFC tag.'
|
||||
})
|
||||
|
||||
readerAbortController.abort()
|
||||
}
|
||||
|
||||
ndef.onreading = ({message, serialNumber}) => {
|
||||
const uid = serialNumber.toUpperCase().replaceAll(':', '')
|
||||
//Decode NDEF data from tag
|
||||
var self = this
|
||||
self.cardDialog.data.uid = serialNumber
|
||||
.toUpperCase()
|
||||
.replaceAll(':', '')
|
||||
this.$q.notify({
|
||||
this.cardDialog.data.uid = uid
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'NFC tag read successfully.'
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
this.nfcTagReading = false
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: error
|
||||
? error.toString()
|
||||
: 'An unexpected error has occurred.'
|
||||
})
|
||||
}
|
||||
},
|
||||
getCards: function () {
|
||||
var self = this
|
||||
|
||||
getCards() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/boltcards/api/v1/cards?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cards = response.data.map(function (obj) {
|
||||
.then(response => {
|
||||
this.cards = response.data.map(function (obj) {
|
||||
return mapCards(obj)
|
||||
})
|
||||
})
|
||||
.then(function () {
|
||||
self.getHits()
|
||||
.then(() => {
|
||||
this.getHits()
|
||||
})
|
||||
},
|
||||
getHits: function () {
|
||||
var self = this
|
||||
getHits() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/boltcards/api/v1/hits?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.hits = response.data.map(function (obj) {
|
||||
obj.card_name = self.cards.find(d => d.id == obj.card_id).card_name
|
||||
.then(response => {
|
||||
this.hits = response.data.map(obj => {
|
||||
obj.card_name = this.cards.find(d => d.id == obj.card_id).card_name
|
||||
return mapCards(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
getRefunds: function () {
|
||||
var self = this
|
||||
getRefunds() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/boltcards/api/v1/refunds?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.refunds = response.data.map(function (obj) {
|
||||
.then(response => {
|
||||
this.refunds = response.data.map(obj => {
|
||||
return mapCards(obj)
|
||||
})
|
||||
})
|
||||
|
|
@ -287,12 +251,11 @@ new Vue({
|
|||
this.qrCodeDialog.wipe = wipe
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
addCardOpen: function () {
|
||||
addCardOpen() {
|
||||
this.cardDialog.show = true
|
||||
this.generateKeys()
|
||||
},
|
||||
generateKeys: function () {
|
||||
var self = this
|
||||
generateKeys() {
|
||||
const genRandomHexBytes = size =>
|
||||
crypto
|
||||
.getRandomValues(new Uint8Array(size))
|
||||
|
|
@ -302,22 +265,22 @@ new Vue({
|
|||
typeof this.cardDialog.data.card_name === 'string' &&
|
||||
this.cardDialog.data.card_name.search('debug') > -1
|
||||
|
||||
self.cardDialog.data.k0 = debugcard
|
||||
this.cardDialog.data.k0 = debugcard
|
||||
? '11111111111111111111111111111111'
|
||||
: genRandomHexBytes(16)
|
||||
|
||||
self.cardDialog.data.k1 = debugcard
|
||||
this.cardDialog.data.k1 = debugcard
|
||||
? '22222222222222222222222222222222'
|
||||
: genRandomHexBytes(16)
|
||||
|
||||
self.cardDialog.data.k2 = debugcard
|
||||
this.cardDialog.data.k2 = debugcard
|
||||
? '33333333333333333333333333333333'
|
||||
: genRandomHexBytes(16)
|
||||
},
|
||||
closeFormDialog: function () {
|
||||
closeFormDialog() {
|
||||
this.cardDialog.data = {}
|
||||
},
|
||||
sendFormData: function () {
|
||||
sendFormData() {
|
||||
let wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.cardDialog.data.wallet
|
||||
})
|
||||
|
|
@ -328,21 +291,17 @@ new Vue({
|
|||
this.createCard(wallet, data)
|
||||
}
|
||||
},
|
||||
createCard: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
createCard(wallet, data) {
|
||||
LNbits.api
|
||||
.request('POST', '/boltcards/api/v1/cards', wallet.adminkey, data)
|
||||
.then(function (response) {
|
||||
self.cards.push(mapCards(response.data))
|
||||
self.cardDialog.show = false
|
||||
self.cardDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
.then(response => {
|
||||
this.cards.push(mapCards(response.data))
|
||||
this.cardDialog.show = false
|
||||
this.cardDialog.data = {}
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
updateCardDialog: function (formId) {
|
||||
updateCardDialog(formId) {
|
||||
var card = _.findWhere(this.cards, {id: formId})
|
||||
this.cardDialog.data = _.clone(card)
|
||||
|
||||
|
|
@ -352,9 +311,7 @@ new Vue({
|
|||
|
||||
this.cardDialog.show = true
|
||||
},
|
||||
updateCard: function (wallet, data) {
|
||||
var self = this
|
||||
|
||||
updateCard(wallet, data) {
|
||||
if (
|
||||
this.cardDialog.temp.k0 != data.k0 ||
|
||||
this.cardDialog.temp.k1 != data.k1 ||
|
||||
|
|
@ -372,21 +329,20 @@ new Vue({
|
|||
wallet.adminkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cards = _.reject(self.cards, function (obj) {
|
||||
.then(response => {
|
||||
this.cards = _.reject(this.cards, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
self.cards.push(mapCards(response.data))
|
||||
self.cardDialog.show = false
|
||||
self.cardDialog.data = {}
|
||||
this.cards.push(mapCards(response.data))
|
||||
this.cardDialog.show = false
|
||||
this.cardDialog.data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
enableCard: function (wallet, card_id, enable) {
|
||||
var self = this
|
||||
let fullWallet = _.findWhere(self.g.user.wallets, {
|
||||
enableCard(wallet, card_id, enable) {
|
||||
let fullWallet = _.findWhere(this.g.user.wallets, {
|
||||
id: wallet
|
||||
})
|
||||
LNbits.api
|
||||
|
|
@ -395,19 +351,18 @@ new Vue({
|
|||
'/boltcards/api/v1/cards/enable/' + card_id + '/' + enable,
|
||||
fullWallet.adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
.then(response => {
|
||||
console.log(response.data)
|
||||
self.cards = _.reject(self.cards, function (obj) {
|
||||
this.cards = _.reject(this.cards, function (obj) {
|
||||
return obj.id == response.data.id
|
||||
})
|
||||
self.cards.push(mapCards(response.data))
|
||||
this.cards.push(mapCards(response.data))
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteCard: function (cardId) {
|
||||
let self = this
|
||||
deleteCard(cardId) {
|
||||
let cards = _.findWhere(this.cards, {id: cardId})
|
||||
|
||||
Quasar.utils.exportFile(
|
||||
|
|
@ -420,15 +375,15 @@ new Vue({
|
|||
.confirmDialog(
|
||||
"Are you sure you want to delete this card? Without access to the card keys you won't be able to reset them in the future!"
|
||||
)
|
||||
.onOk(function () {
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/boltcards/api/v1/cards/' + cardId,
|
||||
_.findWhere(self.g.user.wallets, {id: cards.wallet}).adminkey
|
||||
_.findWhere(this.g.user.wallets, {id: cards.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cards = _.reject(self.cards, function (obj) {
|
||||
.then(response => {
|
||||
this.cards = _.reject(this.cards, function (obj) {
|
||||
return obj.id == cardId
|
||||
})
|
||||
})
|
||||
|
|
@ -437,20 +392,39 @@ new Vue({
|
|||
})
|
||||
})
|
||||
},
|
||||
exportCardsCSV: function () {
|
||||
exportCardsCSV() {
|
||||
LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
|
||||
},
|
||||
exportHitsCSV: function () {
|
||||
exportHitsCSV() {
|
||||
LNbits.utils.exportCSV(this.hitsTable.columns, this.hits)
|
||||
},
|
||||
exportRefundsCSV: function () {
|
||||
exportRefundsCSV() {
|
||||
LNbits.utils.exportCSV(this.refundsTable.columns, this.refunds)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created() {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getCards()
|
||||
this.getRefunds()
|
||||
}
|
||||
try {
|
||||
if (typeof NDEFReader == 'undefined') {
|
||||
throw {
|
||||
toString() {
|
||||
return 'NFC not supported on this device or browser.'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.disableNfcButton = false
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'NFC is supported on this device. You can now read NFC tags.'
|
||||
})
|
||||
} catch (error) {
|
||||
Quasar.Notify.create({
|
||||
type: 'negative',
|
||||
message: error ? error.toString() : 'An unexpected error has occurred.'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
9
tasks.py
9
tasks.py
|
|
@ -1,8 +1,7 @@
|
|||
import asyncio
|
||||
|
||||
from lnbits.core.crud import update_payment_extra
|
||||
from lnbits.core.crud import update_payment
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from .crud import create_refund, get_hit
|
||||
|
|
@ -10,7 +9,7 @@ from .crud import create_refund, get_hit
|
|||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
||||
register_invoice_listener(invoice_queue, "ext_boltcards")
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
|
|
@ -19,7 +18,7 @@ async def wait_for_paid_invoices():
|
|||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
|
||||
if not payment.extra.get("refund"):
|
||||
if not payment.extra or not payment.extra.get("refund"):
|
||||
return
|
||||
|
||||
if payment.extra.get("wh_status"):
|
||||
|
|
@ -31,4 +30,4 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
if hit:
|
||||
await create_refund(hit_id=hit.id, refund_amount=(payment.amount / 1000))
|
||||
payment.extra["wh_status"] = 1
|
||||
await update_payment_extra(payment.payment_hash, payment.extra)
|
||||
await update_payment(payment)
|
||||
|
|
|
|||
|
|
@ -74,20 +74,19 @@ card.card_name }{% endblock %} {% block page %}
|
|||
{% endblock %}{% block scripts %}
|
||||
<script>
|
||||
const mapHits = obj => {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
obj.date = Quasar.date.formatDate(
|
||||
new Date(obj.time),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
|
||||
return obj
|
||||
}
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
delimiters: ['${', '}'],
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
card: null,
|
||||
hits: null,
|
||||
|
|
@ -120,11 +119,11 @@ card.card_name }{% endblock %} {% block page %}
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.card = JSON.parse('{{ card | tojson}}')
|
||||
let hits = JSON.parse('{{ hits | tojson}}')
|
||||
let refunds = JSON.parse('{{ refunds | tojson}}')
|
||||
this.card = JSON.parse({{ card | tojson }})
|
||||
const hits = {{ hits | tojson | safe }}
|
||||
this.hits = hits.map(JSON.parse).map(mapHits)
|
||||
const refunds = {{ refunds | tojson | safe }}
|
||||
this.refunds = refunds || []
|
||||
this.hits = hits.map(mapHits)
|
||||
},
|
||||
computed: {
|
||||
enabled() {
|
||||
|
|
@ -133,8 +132,8 @@ card.card_name }{% endblock %} {% block page %}
|
|||
},
|
||||
methods: {
|
||||
copyText(text, message, position) {
|
||||
Quasar.utils.copyToClipboard(text).then(() => {
|
||||
this.$q.notify({
|
||||
Quasar.copyToClipboard(text).then(() => {
|
||||
Quasar.Notify.create({
|
||||
message: message || 'Copied to clipboard!',
|
||||
position: position || 'bottom'
|
||||
})
|
||||
|
|
|
|||
|
|
@ -34,19 +34,21 @@
|
|||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="cards"
|
||||
:rows="cards"
|
||||
row-key="id"
|
||||
:columns="cardsTable.columns"
|
||||
:pagination.sync="cardsTable.pagination"
|
||||
v-model:pagination="cardsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
|
|
@ -78,9 +80,12 @@
|
|||
<q-tooltip>Card Stats</q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.value"
|
||||
></q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
v-if="props.row.enable"
|
||||
|
|
@ -125,7 +130,6 @@
|
|||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -144,27 +148,31 @@
|
|||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="hits"
|
||||
:rows="hits"
|
||||
row-key="id"
|
||||
:columns="hitsTable.columns"
|
||||
:pagination.sync="hitsTable.pagination"
|
||||
v-model:pagination="hitsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.value"
|
||||
></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -183,27 +191,31 @@
|
|||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="refunds"
|
||||
:rows="refunds"
|
||||
row-key="id"
|
||||
:columns="refundsTable.columns"
|
||||
:pagination.sync="refundsTable.pagination"
|
||||
v-model:pagination="refundsTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.value"
|
||||
></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -240,7 +252,7 @@
|
|||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="cardDialog.data.tx_limit"
|
||||
v-model="cardDialog.data.tx_limit"
|
||||
type="number"
|
||||
label="Max transaction (sats)"
|
||||
class="q-pr-sm"
|
||||
|
|
@ -251,7 +263,7 @@
|
|||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="cardDialog.data.daily_limit"
|
||||
v-model="cardDialog.data.daily_limit"
|
||||
type="number"
|
||||
label="Daily limit (sats)"
|
||||
></q-input>
|
||||
|
|
@ -261,7 +273,7 @@
|
|||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="cardDialog.data.card_name"
|
||||
v-model="cardDialog.data.card_name"
|
||||
type="text"
|
||||
label="Card name "
|
||||
>
|
||||
|
|
@ -272,7 +284,7 @@
|
|||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model.trim="cardDialog.data.uid"
|
||||
v-model="cardDialog.data.uid"
|
||||
type="text"
|
||||
label="Card UID "
|
||||
>
|
||||
|
|
@ -281,36 +293,34 @@
|
|||
<div class="col-2 q-pl-sm">
|
||||
<q-btn
|
||||
outline
|
||||
disable
|
||||
color="grey"
|
||||
icon="nfc"
|
||||
:disable="nfcTagReading"
|
||||
:disable="disableNfcButton"
|
||||
@click="readNfcTag()"
|
||||
>
|
||||
<q-tooltip>Tap card to scan UID</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-toggle
|
||||
v-model="toggleAdvanced"
|
||||
label="Show advanced options"
|
||||
></q-toggle>
|
||||
|
||||
<div v-show="toggleAdvanced" class="q-gutter-y-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="cardDialog.data.k0"
|
||||
v-model="cardDialog.data.k0"
|
||||
type="text"
|
||||
label="Card Auth key (K0)"
|
||||
hint="Used to authentificate with the card (16 bytes in HEX). "
|
||||
@randomkey
|
||||
>
|
||||
</q-input>
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="cardDialog.data.k1"
|
||||
v-model="cardDialog.data.k1"
|
||||
type="text"
|
||||
label="Card Meta key (K1)"
|
||||
hint="Used for encypting of the message (16 bytes in HEX)."
|
||||
|
|
@ -318,12 +328,11 @@
|
|||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="cardDialog.data.k2"
|
||||
v-model="cardDialog.data.k2"
|
||||
type="text"
|
||||
label="Card File key (K2)"
|
||||
hint="Used for CMAC of the message (16 bytes in HEX)."
|
||||
>
|
||||
</q-input>
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
@ -339,8 +348,7 @@
|
|||
unelevated
|
||||
color="primary"
|
||||
class="q-ml-auto"
|
||||
v-on:click="generateKeys"
|
||||
v-on:click.right="debugKeys"
|
||||
@click="generateKeys"
|
||||
>Generate keys</q-btn
|
||||
>
|
||||
</div>
|
||||
|
|
@ -370,19 +378,12 @@
|
|||
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
{% raw %}
|
||||
<div class="col q-mt-lg text-center">
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-xl q-mb-md"
|
||||
v-show="!qrCodeDialog.wipe"
|
||||
>
|
||||
<qrcode
|
||||
<lnbits-qrcode
|
||||
:value="qrCodeDialog.data.link"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
v-show="!qrCodeDialog.wipe"
|
||||
></lnbits-qrcode>
|
||||
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||
(QR for <strong>create</strong> the card in
|
||||
<a
|
||||
|
|
@ -393,17 +394,11 @@
|
|||
>Boltcard NFC Card Creator</a
|
||||
>)
|
||||
</p>
|
||||
<q-responsive
|
||||
:ratio="1"
|
||||
class="q-mx-xl q-mb-md"
|
||||
v-show="qrCodeDialog.wipe"
|
||||
>
|
||||
<qrcode
|
||||
<lnbits-qrcode
|
||||
:value="qrCodeDialog.data_wipe"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
v-show="qrCodeDialog.wipe"
|
||||
></lnbits-qrcode>
|
||||
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||
(QR for <strong>wipe</strong> the card in
|
||||
<a
|
||||
|
|
@ -430,12 +425,18 @@
|
|||
/>
|
||||
</div>
|
||||
<p style="word-break: break-all">
|
||||
<strong>Name:</strong> {{ qrCodeDialog.data.name }}<br />
|
||||
<strong>UID:</strong> {{ qrCodeDialog.data.uid }}<br />
|
||||
<strong>External ID:</strong> {{ qrCodeDialog.data.external_id }}<br />
|
||||
<strong>Lock key (K0):</strong> {{ qrCodeDialog.data.k0 }}<br />
|
||||
<strong>Meta key (K1 & K3):</strong> {{ qrCodeDialog.data.k1 }}<br />
|
||||
<strong>File key (K2 & K4):</strong> {{ qrCodeDialog.data.k2 }}<br />
|
||||
<strong>Name: </strong><span v-text="qrCodeDialog.data.name"></span
|
||||
><br />
|
||||
<strong>UID: </strong> <span v-text="qrCodeDialog.data.uid"></span
|
||||
><br />
|
||||
<strong>External ID:</strong>
|
||||
<span v-text="qrCodeDialog.data.external_id"></span><br />
|
||||
<strong>Lock key (K0):</strong>
|
||||
<span v-text="qrCodeDialog.data.k0"></span><br />
|
||||
<strong>Meta key (K1 & K3):</strong>
|
||||
<span v-text="qrCodeDialog.data.k1"></span><br />
|
||||
<strong>File key (K2 & K4):</strong>
|
||||
<span v-text="qrCodeDialog.data.k2"></span><br />
|
||||
</p>
|
||||
<p>
|
||||
Always backup all keys that you're trying to write on the card. Without
|
||||
|
|
@ -472,7 +473,6 @@
|
|||
>
|
||||
<q-tooltip>Backup the keys, or wipe the card first!</q-tooltip>
|
||||
</q-btn>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
|
|
@ -481,5 +481,5 @@
|
|||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="/boltcards/static/js/index.js"></script>
|
||||
<script src="{{ static_url_for('boltcards/static', path='js/index.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
23
views.py
23
views.py
|
|
@ -1,16 +1,13 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from .crud import get_card_by_external_id, get_hits, get_refunds
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
boltcards_generic_router = APIRouter()
|
||||
|
||||
|
||||
|
|
@ -21,7 +18,7 @@ def boltcards_renderer():
|
|||
@boltcards_generic_router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return boltcards_renderer().TemplateResponse(
|
||||
"boltcards/index.html", {"request": request, "user": user.dict()}
|
||||
"boltcards/index.html", {"request": request, "user": user.json()}
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -32,15 +29,11 @@ async def display(request: Request, card_id: str):
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Card does not exist."
|
||||
)
|
||||
hits = [hit.dict() for hit in await get_hits([card.id])]
|
||||
refunds = [
|
||||
refund.hit_id for refund in await get_refunds([hit["id"] for hit in hits])
|
||||
]
|
||||
card_dict = card.dict()
|
||||
# Remove wallet id from card dict
|
||||
del card_dict["wallet"]
|
||||
|
||||
hits = await get_hits([card.id])
|
||||
hits_json = [hit.json() for hit in hits]
|
||||
refunds = [refund.hit_id for refund in await get_refunds([hit.id for hit in hits])]
|
||||
card_json = card.json(exclude={"wallet"})
|
||||
return boltcards_renderer().TemplateResponse(
|
||||
"boltcards/display.html",
|
||||
{"request": request, "card": card_dict, "hits": hits, "refunds": refunds},
|
||||
{"request": request, "card": card_json, "hits": hits_json, "refunds": refunds},
|
||||
)
|
||||
|
|
|
|||
36
views_api.py
36
views_api.py
|
|
@ -3,7 +3,7 @@ from http import HTTPStatus
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.decorators import get_key_type, require_admin_key
|
||||
from lnbits.decorators import require_admin_key, require_invoice_key
|
||||
|
||||
from .crud import (
|
||||
create_card,
|
||||
|
|
@ -16,22 +16,22 @@ from .crud import (
|
|||
get_refunds,
|
||||
update_card,
|
||||
)
|
||||
from .models import Card, CreateCardData
|
||||
from .models import Card, CreateCardData, Hit, Refund
|
||||
|
||||
boltcards_api_router = APIRouter()
|
||||
|
||||
|
||||
@boltcards_api_router.get("/api/v1/cards")
|
||||
async def api_cards(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = False
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key), all_wallets: bool = False
|
||||
) -> list[Card]:
|
||||
wallet_ids = [key_info.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
user = await get_user(g.wallet.user)
|
||||
user = await get_user(key_info.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return [card.dict() for card in await get_cards(wallet_ids)]
|
||||
return await get_cards(wallet_ids)
|
||||
|
||||
|
||||
def validate_card(data: CreateCardData):
|
||||
|
|
@ -146,12 +146,13 @@ async def api_card_delete(card_id, wallet: WalletTypeInfo = Depends(require_admi
|
|||
|
||||
@boltcards_api_router.get("/api/v1/hits")
|
||||
async def api_hits(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
all_wallets: bool = Query(False),
|
||||
) -> list[Hit]:
|
||||
wallet_ids = [key_info.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
user = await get_user(g.wallet.user)
|
||||
user = await get_user(key_info.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
cards = await get_cards(wallet_ids)
|
||||
|
|
@ -159,17 +160,18 @@ async def api_hits(
|
|||
for card in cards:
|
||||
cards_ids.append(card.id)
|
||||
|
||||
return [hit.dict() for hit in await get_hits(cards_ids)]
|
||||
return await get_hits(cards_ids)
|
||||
|
||||
|
||||
@boltcards_api_router.get("/api/v1/refunds")
|
||||
async def api_refunds(
|
||||
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||
):
|
||||
wallet_ids = [g.wallet.id]
|
||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||
all_wallets: bool = Query(False),
|
||||
) -> list[Refund]:
|
||||
wallet_ids = [key_info.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
user = await get_user(g.wallet.user)
|
||||
user = await get_user(key_info.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
cards = await get_cards(wallet_ids)
|
||||
|
|
@ -181,4 +183,4 @@ async def api_refunds(
|
|||
for hit in hits:
|
||||
hits_ids.append(hit.id)
|
||||
|
||||
return [refund.dict() for refund in await get_refunds(hits_ids)]
|
||||
return await get_refunds(hits_ids)
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ async def api_scan(p, c, request: Request, external_id: str):
|
|||
|
||||
hits_amount = 0
|
||||
for hit in todays_hits:
|
||||
hits_amount = hits_amount + hit.amount
|
||||
if hits_amount > card.daily_limit:
|
||||
hits_amount += hit.amount
|
||||
if hits_amount > int(card.daily_limit):
|
||||
return {"status": "ERROR", "reason": "Max daily limit spent."}
|
||||
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ async def api_scan(p, c, request: Request, external_id: str):
|
|||
"callback": str(request.url_for("boltcards.lnurl_callback", hit_id=hit.id)),
|
||||
"k1": hit.id,
|
||||
"minWithdrawable": 1 * 1000,
|
||||
"maxWithdrawable": card.tx_limit * 1000,
|
||||
"maxWithdrawable": int(card.tx_limit) * 1000,
|
||||
"defaultDescription": f"Boltcard (refund address lnurl://{lnurlpay_bech32})",
|
||||
"payLink": lnurlpay_nonbech32_lud17, # LUD-19 compatibility
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ async def lnurl_callback(
|
|||
await pay_invoice(
|
||||
wallet_id=card.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=card.tx_limit,
|
||||
max_sat=int(card.tx_limit),
|
||||
extra={"tag": "boltcards", "hit": hit.id},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
|
|
@ -201,14 +201,13 @@ async def lnurlp_response(req: Request, hit_id: str):
|
|||
"callback": str(req.url_for("boltcards.lnurlp_callback", hit_id=hit_id)),
|
||||
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
|
||||
"minSendable": 1 * 1000,
|
||||
"maxSendable": card.tx_limit * 1000,
|
||||
"maxSendable": int(card.tx_limit) * 1000,
|
||||
}
|
||||
return json.dumps(pay_response)
|
||||
|
||||
|
||||
@boltcards_lnurl_router.get(
|
||||
"/api/v1/lnurlp/cb/{hit_id}",
|
||||
response_class=HTMLResponse,
|
||||
name="boltcards.lnurlp_callback",
|
||||
)
|
||||
async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
||||
|
|
@ -219,7 +218,7 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
|||
if not hit:
|
||||
return {"status": "ERROR", "reason": "LNURL-pay record not found."}
|
||||
|
||||
_, payment_request = await create_invoice(
|
||||
payment = await create_invoice(
|
||||
wallet_id=card.wallet,
|
||||
amount=int(int(amount) / 1000),
|
||||
memo=f"Refund {hit_id}",
|
||||
|
|
@ -229,6 +228,4 @@ async def lnurlp_callback(hit_id: str, amount: str = Query(None)):
|
|||
extra={"refund": hit_id},
|
||||
)
|
||||
|
||||
pay_response = {"pr": payment_request, "routes": []}
|
||||
|
||||
return json.dumps(pay_response)
|
||||
return {"pr": payment.bolt11, "routes": []}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue