Pretty much works

This commit is contained in:
ben 2022-08-26 19:22:03 +01:00
parent c3870e65e3
commit 330a026379
6 changed files with 54 additions and 29 deletions

View file

@ -8,30 +8,32 @@ from .models import Card, CreateCardData, Hit, Refund
async def create_card(data: CreateCardData, wallet_id: str) -> Card: async def create_card(data: CreateCardData, wallet_id: str) -> Card:
card_id = urlsafe_short_hash() card_id = urlsafe_short_hash().upper()
await db.execute( await db.execute(
""" """
INSERT INTO boltcards.cards ( INSERT INTO boltcards.cards (
id, id,
uid,
wallet, wallet,
card_name, card_name,
uid,
counter, counter,
withdraw, tx_limit,
daily_limit,
k0, k0,
k1, k1,
k2, k2,
otp otp
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
card_id, card_id,
data.uid.upper(),
wallet_id, wallet_id,
data.card_name, data.card_name,
data.uid.upper(),
data.counter, data.counter,
data.withdraw, data.tx_limit,
data.daily_limit,
data.k0, data.k0,
data.k1, data.k1,
data.k2, data.k2,
@ -145,11 +147,16 @@ async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
async def get_hits_today(card_id: Union[str, List[str]]) -> List[Hit]: async def get_hits_today(card_id: Union[str, List[str]]) -> List[Hit]:
rows = await db.fetchall( rows = await db.fetchall(
f"SELECT * FROM boltcards.hits WHERE card_id = ? AND timestamp >= DATE() AND timestamp < DATE() + INTERVAL ? DAY", (card_id, 1) f"SELECT * FROM boltcards.hits WHERE card_id = ? AND time >= DATE('now') AND time < DATE('now', '+1 day')", (card_id,)
) )
return [Hit(**row) for row in rows] return [Hit(**row) for row in rows]
async def spend_hit(id: str):
await db.execute(
"UPDATE boltcards.hits SET spent = ? WHERE id = ?",
(True, id),
)
async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit: async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
hit_id = urlsafe_short_hash() hit_id = urlsafe_short_hash()
@ -159,19 +166,23 @@ async def create_hit(card_id, ip, useragent, old_ctr, new_ctr) -> Hit:
id, id,
card_id, card_id,
ip, ip,
spent,
useragent, useragent,
old_ctr, old_ctr,
new_ctr new_ctr,
amount
) )
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
hit_id, hit_id,
card_id, card_id,
ip, ip,
False,
useragent, useragent,
old_ctr, old_ctr,
new_ctr, new_ctr,
0,
), ),
) )
hit = await get_hit(hit_id) hit = await get_hit(hit_id)

View file

@ -1,10 +1,13 @@
import base64 import base64
import hashlib import hashlib
import hmac import hmac
import json
from http import HTTPStatus from http import HTTPStatus
from io import BytesIO from io import BytesIO
from typing import Optional from typing import Optional
from loguru import logger
from embit import bech32, compact from embit import bech32, compact
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
@ -33,6 +36,7 @@ from .crud import (
get_card_by_uid, get_card_by_uid,
get_hit, get_hit,
get_hits_today, get_hits_today,
spend_hit,
update_card, update_card,
update_card_counter, update_card_counter,
update_card_otp, update_card_otp,
@ -50,10 +54,10 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
c = c.upper() c = c.upper()
card = None card = None
counter = b"" counter = b""
try: try:
card = await get_card_by_uid(card_uid) card = await get_card_by_uid(card_uid)
card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1)) card_uid, counter = decryptSUN(bytes.fromhex(p), bytes.fromhex(card.k1))
if card.uid.upper() != card_uid.hex().upper(): if card.uid.upper() != card_uid.hex().upper():
return {"status": "ERROR", "reason": "Card UID mis-match."} return {"status": "ERROR", "reason": "Card UID mis-match."}
except: except:
@ -67,8 +71,8 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
ctr_int = int.from_bytes(counter, "little") ctr_int = int.from_bytes(counter, "little")
if ctr_int <= card.counter: # if ctr_int <= card.counter:
return {"status": "ERROR", "reason": "This link is already used."} # return {"status": "ERROR", "reason": "This link is already used."}
await update_card_counter(ctr_int, card.id) await update_card_counter(ctr_int, card.id)
@ -86,13 +90,13 @@ async def api_scan(p, c, request: Request, card_uid: str = None):
for hit in todays_hits: for hit in todays_hits:
hits_amount = hits_amount + hit.amount hits_amount = hits_amount + hit.amount
if (hits_amount + card.tx_limit) > card.daily_limit: if (hits_amount + card.tx_limit) > card.daily_limit:
return {"status": "ERROR", "reason": "Max daily liit spent."} return {"status": "ERROR", "reason": "Max daily limit spent."}
hit = await create_hit(card.id, ip, agent, card.counter, ctr_int) hit = await create_hit(card.id, ip, agent, card.counter, ctr_int)
lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id)) lnurlpay = lnurl_encode(request.url_for("boltcards.lnurlp_response", hit_id=hit.id))
return { return {
"tag": "withdrawRequest", "tag": "withdrawRequest",
"callback": request.url_for( "callback": request.url_for(
"boltcards.lnurl_callback" "boltcards.lnurl_callback", hitid=hit.id
), ),
"k1": hit.id, "k1": hit.id,
"minWithdrawable": 1 * 1000, "minWithdrawable": 1 * 1000,
@ -166,14 +170,15 @@ async def api_auth(a, request: Request):
) )
async def lnurlp_response(req: Request, hit_id: str = Query(None)): async def lnurlp_response(req: Request, hit_id: str = Query(None)):
hit = await get_hit(hit_id) hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
if not hit: if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."} return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
payResponse = { payResponse = {
"tag": "payRequest", "tag": "payRequest",
"callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id), "callback": req.url_for("boltcards.lnurlp_callback", hit_id=hit_id),
"metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])), "metadata": LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])),
"minSendable": math.ceil(link.min_bet * 1) * 1000, "minSendable": 1 * 1000,
"maxSendable": round(link.max_bet * 1) * 1000, "maxSendable": card.tx_limit * 1000,
} }
return json.dumps(payResponse) return json.dumps(payResponse)
@ -187,14 +192,15 @@ async def lnurlp_callback(
req: Request, hit_id: str = Query(None), amount: str = Query(None) req: Request, hit_id: str = Query(None), amount: str = Query(None)
): ):
hit = await get_hit(hit_id) hit = await get_hit(hit_id)
card = await get_card(hit.card_id)
if not hit: if not hit:
return {"status": "ERROR", "reason": f"LNURL-pay record not found."} return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=link.wallet, wallet_id=card.wallet,
amount=int(amount / 1000), amount=int(amount) / 1000,
memo=f"Refund {hit_id}", memo=f"Refund {hit_id}",
unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", hit_id]])).encode("utf-8"), unhashed_description=LnurlPayMetadata(json.dumps([["text/plain", "Refund"]])).encode("utf-8"),
extra={"refund": hit_id}, extra={"refund": hit_id},
) )

View file

@ -64,6 +64,7 @@ class Hit(BaseModel):
useragent: str useragent: str
old_ctr: int old_ctr: int
new_ctr: int new_ctr: int
amount: int
time: int time: int
def from_row(cls, row: Row) -> "Hit": def from_row(cls, row: Row) -> "Hit":

View file

@ -18,6 +18,7 @@ new Vue({
cards: [], cards: [],
hits: [], hits: [],
refunds: [], refunds: [],
lnurlLink: location.hostname + '/boltcards/api/v1/scan/',
cardDialog: { cardDialog: {
show: false, show: false,
data: { data: {
@ -43,10 +44,10 @@ new Vue({
field: 'counter' field: 'counter'
}, },
{ {
name: 'withdraw', name: 'uid',
align: 'left', align: 'left',
label: 'Withdraw ID', label: 'Card ID',
field: 'withdraw' field: 'uid'
} }
], ],
pagination: { pagination: {
@ -150,7 +151,6 @@ new Vue({
}, },
getHits: function () { getHits: function () {
var self = this var self = this
LNbits.api LNbits.api
.request( .request(
'GET', 'GET',
@ -167,7 +167,6 @@ new Vue({
}, },
getRefunds: function () { getRefunds: function () {
var self = this var self = this
LNbits.api LNbits.api
.request( .request(
'GET', 'GET',
@ -184,7 +183,6 @@ new Vue({
}, },
openQrCodeDialog(cardId) { openQrCodeDialog(cardId) {
var card = _.findWhere(this.cards, {id: cardId}) var card = _.findWhere(this.cards, {id: cardId})
this.qrCodeDialog.data = { this.qrCodeDialog.data = {
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp, link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
name: card.card_name, name: card.card_name,
@ -197,11 +195,9 @@ new Vue({
}, },
addCardOpen: function () { addCardOpen: function () {
this.cardDialog.show = true this.cardDialog.show = true
var elem = this.$els.myBtn this.generateKeys()
elem.click()
}, },
generateKeys: function () { generateKeys: function () {
const genRanHex = size => const genRanHex = size =>
[...Array(size)] [...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16)) .map(() => Math.floor(Math.random() * 16).toString(16))

View file

@ -34,6 +34,7 @@
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th> <q-th auto-width></q-th>
<q-th auto-width>Base URL</q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} {{ col.label }}
</q-th> </q-th>
@ -53,6 +54,14 @@
@click="openQrCodeDialog(props.row.id)" @click="openQrCodeDialog(props.row.id)"
></q-btn> ></q-btn>
</q-td> </q-td>
<q-td auto-width>
<q-btn
outline
color="grey"
@click="copyText(lnurlLink + props.row.uid)"
lnurlLink >lnurl://...<q-tooltip>Click to copy, then add to NFC card</q-tooltip>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }} {{ col.value }}
</q-td> </q-td>
@ -193,7 +202,7 @@
filled filled
dense dense
emit-value emit-value
v-model.trim="cardDialog.data.trans_limit" v-model.trim="cardDialog.data.tx_limit"
type="number" type="number"
label="Max transaction (sats)" label="Max transaction (sats)"
class="q-pr-sm" class="q-pr-sm"

View file

@ -26,6 +26,7 @@ from .crud import (
from .models import CreateCardData from .models import CreateCardData
from .nxp424 import decryptSUN, getSunMAC from .nxp424 import decryptSUN, getSunMAC
from loguru import logger
@boltcards_ext.get("/api/v1/cards") @boltcards_ext.get("/api/v1/cards")
async def api_cards( async def api_cards(
@ -47,6 +48,7 @@ async def api_card_create_or_update(
card_id: str = None, card_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
logger.debug(len(bytes.fromhex(data.uid)))
try: try:
if len(bytes.fromhex(data.uid)) != 7: if len(bytes.fromhex(data.uid)) != 7:
raise HTTPException( raise HTTPException(