Pretty much works
This commit is contained in:
parent
c3870e65e3
commit
330a026379
6 changed files with 54 additions and 29 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue