new extension
just proof of concept
This commit is contained in:
parent
c32ff1de59
commit
4fab2d3101
11 changed files with 780 additions and 0 deletions
11
lnbits/extensions/boltcards/README.md
Normal file
11
lnbits/extensions/boltcards/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<h1>boltcards Extension</h1>
|
||||||
|
<h2>*tagline*</h2>
|
||||||
|
This is an boltcards extension to help you organise and build you own.
|
||||||
|
|
||||||
|
Try to include an image
|
||||||
|
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||||
|
|
||||||
|
|
||||||
|
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||||
|
|
||||||
|
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/boltcards -d '{"amount":"100","memo":"boltcards"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||||
19
lnbits/extensions/boltcards/__init__.py
Normal file
19
lnbits/extensions/boltcards/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
|
db = Database("ext_boltcards")
|
||||||
|
|
||||||
|
boltcards_ext: APIRouter = APIRouter(
|
||||||
|
prefix="/boltcards",
|
||||||
|
tags=["boltcards"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def boltcards_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/boltcards/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
6
lnbits/extensions/boltcards/config.json
Normal file
6
lnbits/extensions/boltcards/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Bolt Cards",
|
||||||
|
"short_description": "Self custody Bolt Cards with one time LNURLw",
|
||||||
|
"icon": "payment",
|
||||||
|
"contributors": ["iwarpbtc"]
|
||||||
|
}
|
||||||
91
lnbits/extensions/boltcards/crud.py
Normal file
91
lnbits/extensions/boltcards/crud.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
from optparse import Option
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import Card, CreateCardData
|
||||||
|
|
||||||
|
async def create_card(
|
||||||
|
data: CreateCardData, wallet_id: str
|
||||||
|
) -> Card:
|
||||||
|
card_id = urlsafe_short_hash()
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO boltcards.cards (
|
||||||
|
id,
|
||||||
|
wallet,
|
||||||
|
card_name,
|
||||||
|
uid,
|
||||||
|
counter,
|
||||||
|
withdraw,
|
||||||
|
file_key,
|
||||||
|
meta_key
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
card_id,
|
||||||
|
wallet_id,
|
||||||
|
data.name,
|
||||||
|
data.uid,
|
||||||
|
data.counter,
|
||||||
|
data.withdraw,
|
||||||
|
data.file_key,
|
||||||
|
data.meta_key,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
link = await get_card(card_id, 0)
|
||||||
|
assert link, "Newly created card couldn't be retrieved"
|
||||||
|
return link
|
||||||
|
|
||||||
|
async def update_card(card_id: str, **kwargs) -> Optional[Card]:
|
||||||
|
if "is_unique" in kwargs:
|
||||||
|
kwargs["is_unique"] = int(kwargs["is_unique"])
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM boltcards.cards WHERE id = ?", (card_id,)
|
||||||
|
)
|
||||||
|
return Card(**row) if row else None
|
||||||
|
|
||||||
|
async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]:
|
||||||
|
if isinstance(wallet_ids, str):
|
||||||
|
wallet_ids = [wallet_ids]
|
||||||
|
|
||||||
|
q = ",".join(["?"] * len(wallet_ids))
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT * FROM boltcards.cards WHERE wallet IN ({q})", (*wallet_ids,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Card(**row) for row in rows]
|
||||||
|
|
||||||
|
async def get_all_cards() -> List[Card]:
|
||||||
|
rows = await db.fetchall(
|
||||||
|
f"SELECT * FROM boltcards.cards"
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Card(**row) for row in rows]
|
||||||
|
|
||||||
|
async def get_card(card_id: str, id_is_uid: bool=False) -> Optional[Card]:
|
||||||
|
sql = "SELECT * FROM boltcards.cards WHERE {} = ?".format("uid" if id_is_uid else "id")
|
||||||
|
row = await db.fetchone(
|
||||||
|
sql, card_id,
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
card = dict(**row)
|
||||||
|
|
||||||
|
return Card.parse_obj(card)
|
||||||
|
|
||||||
|
async def delete_card(card_id: str) -> None:
|
||||||
|
await db.execute("DELETE FROM boltcards.cards WHERE id = ?", (card_id,))
|
||||||
|
|
||||||
|
async def update_card_counter(counter: int, id: str):
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE boltcards.cards SET counter = ? WHERE id = ?",
|
||||||
|
(counter, id),
|
||||||
|
)
|
||||||
20
lnbits/extensions/boltcards/migrations.py
Normal file
20
lnbits/extensions/boltcards/migrations.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
async def m001_initial(db):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE boltcards.cards (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
card_name TEXT NOT NULL,
|
||||||
|
uid TEXT NOT NULL,
|
||||||
|
counter INT NOT NULL DEFAULT 0,
|
||||||
|
withdraw TEXT NOT NULL,
|
||||||
|
file_key TEXT NOT NULL DEFAULT '00000000000000000000000000000000',
|
||||||
|
meta_key TEXT NOT NULL DEFAULT '',
|
||||||
|
time TIMESTAMP NOT NULL DEFAULT """
|
||||||
|
+ db.timestamp_now
|
||||||
|
+ """
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
21
lnbits/extensions/boltcards/models.py
Normal file
21
lnbits/extensions/boltcards/models.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi.params import Query
|
||||||
|
|
||||||
|
class Card(BaseModel):
|
||||||
|
id: str
|
||||||
|
wallet: str
|
||||||
|
card_name: str
|
||||||
|
uid: str
|
||||||
|
counter: int
|
||||||
|
withdraw: str
|
||||||
|
file_key: str
|
||||||
|
meta_key: str
|
||||||
|
time: int
|
||||||
|
|
||||||
|
class CreateCardData(BaseModel):
|
||||||
|
card_name: str = Query(...)
|
||||||
|
uid: str = Query(...)
|
||||||
|
counter: str = Query(...)
|
||||||
|
withdraw: str = Query(...)
|
||||||
|
file_key: str = Query(...)
|
||||||
|
meta_key: str = Query(...)
|
||||||
31
lnbits/extensions/boltcards/nxp424.py
Normal file
31
lnbits/extensions/boltcards/nxp424.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from typing import Tuple
|
||||||
|
from Cryptodome.Hash import CMAC
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
|
||||||
|
SV2 = "3CC300010080"
|
||||||
|
|
||||||
|
def myCMAC(key: bytes, msg: bytes=b'') -> bytes:
|
||||||
|
cobj = CMAC.new(key, ciphermod=AES)
|
||||||
|
if msg != b'':
|
||||||
|
cobj.update(msg)
|
||||||
|
return cobj.digest()
|
||||||
|
|
||||||
|
def decryptSUN(sun: bytes, key: bytes) -> Tuple[bytes, bytes]:
|
||||||
|
IVbytes = b"\x00" * 16
|
||||||
|
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, IVbytes)
|
||||||
|
sun_plain = cipher.decrypt(sun)
|
||||||
|
|
||||||
|
UID = sun_plain[1:8]
|
||||||
|
counter = sun_plain[8:11]
|
||||||
|
|
||||||
|
return UID, counter
|
||||||
|
|
||||||
|
def getSunMAC(UID: bytes, counter: bytes, key: bytes) -> bytes:
|
||||||
|
sv2prefix = bytes.fromhex(SV2)
|
||||||
|
sv2bytes = sv2prefix + UID + counter
|
||||||
|
|
||||||
|
mac1 = myCMAC(key, sv2bytes)
|
||||||
|
mac2 = myCMAC(mac1)
|
||||||
|
|
||||||
|
return mac2[1::2]
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="swap_vertical_circle"
|
||||||
|
label="About Bolt Cards"
|
||||||
|
:content-inset-level="0.5"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h5 class="text-subtitle1 q-my-none">
|
||||||
|
Be your own card association
|
||||||
|
</h5>
|
||||||
|
<p>
|
||||||
|
Manage your Bolt Cards self custodian way<br />
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com/lnbits/lnbits/tree/master/lnbits/extensions/boltcards"
|
||||||
|
>More details</a
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
Created by,
|
||||||
|
<a href="https://twitter.com/btcslovnik">iWarp</a></small
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-expansion-item>
|
||||||
375
lnbits/extensions/boltcards/templates/boltcards/index.html
Normal file
375
lnbits/extensions/boltcards/templates/boltcards/index.html
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block page %}
|
||||||
|
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn unelevated color="primary" @click="cardDialog.show = true"
|
||||||
|
>Add Card</q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">Cards</h5>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-btn flat color="grey" @click="exportCardsCSV"
|
||||||
|
>Export to CSV</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
:data="cards"
|
||||||
|
row-key="id"
|
||||||
|
:columns="cardsTable.columns"
|
||||||
|
:pagination.sync="cardsTable.pagination"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<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 auto-width></q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
icon="link"
|
||||||
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
|
type="a"
|
||||||
|
:href="props.row.displayUrl"
|
||||||
|
target="_blank"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
|
{{ col.value }}
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="updateCardDialog(props.row.id)"
|
||||||
|
icon="edit"
|
||||||
|
color="light-blue"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteCard(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle1 q-my-none">
|
||||||
|
{{SITE_TITLE}} Bolt Cards extension
|
||||||
|
</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-list> {% include "boltcards/_api_docs.html" %} </q-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<q-dialog v-model="cardDialog.show" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="cardDialog.data.wallet"
|
||||||
|
:options="g.user.walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="cardDialog.data.withdraw"
|
||||||
|
:options="withdrawsOptions"
|
||||||
|
label="Withdraw link *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model.trim="cardDialog.data.card_name"
|
||||||
|
type="text"
|
||||||
|
label="Card name "
|
||||||
|
><q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||||
|
>The domain to use ex: "example.com"</q-tooltip
|
||||||
|
></q-input
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
bottom-slots
|
||||||
|
v-model.trim="cardDialog.data.uid"
|
||||||
|
type="text"
|
||||||
|
label="Card UID"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="cardDialog.data.file_key"
|
||||||
|
type="text"
|
||||||
|
label="Card File key"
|
||||||
|
>
|
||||||
|
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||||
|
>Create a "Edit zone DNS" API token in cloudflare</q-tooltip
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="cardDialog.data.meta_key"
|
||||||
|
type="text"
|
||||||
|
label="Card Meta key"
|
||||||
|
hint="A URL to be called whenever this link receives a payment."
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="cardDialog.data.counter"
|
||||||
|
type="number"
|
||||||
|
label="Initial counter"
|
||||||
|
><q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||||
|
>How much to charge per day</q-tooltip
|
||||||
|
></q-input
|
||||||
|
>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-if="cardDialog.data.id"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>Update Form</q-btn
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="cardDialog.data.uid == null"
|
||||||
|
type="submit"
|
||||||
|
>Create Card</q-btn
|
||||||
|
>
|
||||||
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
const mapCards = obj => {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.time * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
|
||||||
|
obj.displayUrl = ['/boltcards/', obj.id].join('')
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
cards: [],
|
||||||
|
withdrawsOptions: [],
|
||||||
|
cardDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
cardsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'card_name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Card name',
|
||||||
|
field: 'card_name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'counter',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Counter',
|
||||||
|
field: 'counter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'withdraw',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Withdraw ID',
|
||||||
|
field: 'withdraw'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCards: function () {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
return mapCards(obj)
|
||||||
|
})
|
||||||
|
console.log(self.cards)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getWithdraws: function () {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/withdraw/api/v1/links?all_wallets=true',
|
||||||
|
this.g.user.wallets[0].inkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.withdrawsOptions = response.data
|
||||||
|
.map(function (obj) {
|
||||||
|
return {
|
||||||
|
label: [obj.title, ' - ', obj.id].join(''),
|
||||||
|
value: obj.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(self.withdraws)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendFormData: function () {
|
||||||
|
let wallet = _.findWhere(this.g.user.wallets, {
|
||||||
|
id: this.cardDialog.data.wallet
|
||||||
|
})
|
||||||
|
let data = this.cardDialog.data
|
||||||
|
if (data.id) {
|
||||||
|
this.updateCard(wallet, data)
|
||||||
|
} else {
|
||||||
|
this.createCard(wallet, data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createCard: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateCardDialog: function (formId) {
|
||||||
|
var card = _.findWhere(this.cards, {id: formId})
|
||||||
|
console.log(card.id)
|
||||||
|
this.cardDialog.data = _.clone(card)
|
||||||
|
this.cardDialog.show = true
|
||||||
|
},
|
||||||
|
updateCard: function (wallet, data) {
|
||||||
|
var self = this
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'PUT',
|
||||||
|
'/boltcards/api/v1/cards/' + data.id,
|
||||||
|
wallet.adminkey,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.cards = _.reject(self.cards, function (obj) {
|
||||||
|
return obj.id == data.id
|
||||||
|
})
|
||||||
|
self.cards.push(mapCards(response.data))
|
||||||
|
self.cardDialog.show = false
|
||||||
|
self.cardDialog.data = {}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteCard: function (cardId) {
|
||||||
|
let self = this
|
||||||
|
let cards = _.findWhere(this.cards, {id: cardId})
|
||||||
|
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this card')
|
||||||
|
.onOk(function () {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/boltcards/api/v1/cards/' + cardId,
|
||||||
|
_.findWhere(self.g.user.wallets, {id: cards.wallet}).adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
self.cards = _.reject(self.cards, function (obj) {
|
||||||
|
return obj.id == cardId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportCardsCSV: function () {
|
||||||
|
LNbits.utils.exportCSV(this.cardsTable.columns, this.cards)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
if (this.g.user.wallets.length) {
|
||||||
|
this.getCards()
|
||||||
|
this.getWithdraws()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
18
lnbits/extensions/boltcards/views.py
Normal file
18
lnbits/extensions/boltcards/views.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
|
from . import boltcards_ext, boltcards_renderer
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@boltcards_ext.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()}
|
||||||
|
)
|
||||||
161
lnbits/extensions/boltcards/views_api.py
Normal file
161
lnbits/extensions/boltcards/views_api.py
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# views_api.py is for you API endpoints that could be hit by another service
|
||||||
|
|
||||||
|
# add your dependencies here
|
||||||
|
|
||||||
|
# import httpx
|
||||||
|
# (use httpx just like requests, except instead of response.ok there's only the
|
||||||
|
# response.is_error that is its inverse)
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi.params import Depends, Query
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from lnbits.core.crud import get_user
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
from lnbits.extensions.withdraw import get_withdraw_link
|
||||||
|
|
||||||
|
from . import boltcards_ext
|
||||||
|
from .nxp424 import decryptSUN, getSunMAC
|
||||||
|
from .crud import (
|
||||||
|
get_all_cards,
|
||||||
|
get_cards,
|
||||||
|
get_card,
|
||||||
|
create_card,
|
||||||
|
update_card,
|
||||||
|
delete_card,
|
||||||
|
update_card_counter
|
||||||
|
)
|
||||||
|
from .models import CreateCardData
|
||||||
|
|
||||||
|
@boltcards_ext.get("/api/v1/cards")
|
||||||
|
async def api_cards(
|
||||||
|
g: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)
|
||||||
|
):
|
||||||
|
wallet_ids = [g.wallet.id]
|
||||||
|
|
||||||
|
if all_wallets:
|
||||||
|
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
|
||||||
|
|
||||||
|
return [card.dict() for card in await get_cards(wallet_ids)]
|
||||||
|
|
||||||
|
@boltcards_ext.post("/api/v1/cards", status_code=HTTPStatus.CREATED)
|
||||||
|
@boltcards_ext.put("/api/v1/cards/{card_id}", status_code=HTTPStatus.OK)
|
||||||
|
async def api_link_create_or_update(
|
||||||
|
req: Request,
|
||||||
|
data: CreateCardData,
|
||||||
|
card_id: str = None,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
if data.uses > 250:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="250 uses max.", status_code=HTTPStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.min_withdrawable < 1:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.max_withdrawable < data.min_withdrawable:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
if card_id:
|
||||||
|
card = await get_card(card_id)
|
||||||
|
if not card:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||||
|
)
|
||||||
|
if card.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
|
||||||
|
)
|
||||||
|
card = await update_card(
|
||||||
|
card_id, **data.dict()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
card = await create_card(
|
||||||
|
wallet_id=wallet.wallet.id, data=data
|
||||||
|
)
|
||||||
|
return card.dict()
|
||||||
|
|
||||||
|
@boltcards_ext.delete("/api/v1/cards/{card_id}")
|
||||||
|
async def api_link_delete(card_id, wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||||
|
card = await get_card(card_id)
|
||||||
|
|
||||||
|
if not card:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Card does not exist.", status_code=HTTPStatus.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
if card.wallet != wallet.wallet.id:
|
||||||
|
raise HTTPException(
|
||||||
|
detail="Not your card.", status_code=HTTPStatus.FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
await delete_card(card_id)
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
@boltcards_ext.get("/api/v1/scan/") # pay.btcslovnik.cz/boltcards/api/v1/scan/?uid=00000000000000&ctr=000000&c=0000000000000000
|
||||||
|
async def api_scan(
|
||||||
|
uid, ctr, c,
|
||||||
|
request: Request
|
||||||
|
):
|
||||||
|
card = await get_card(uid, id_is_uid=True)
|
||||||
|
|
||||||
|
if card == None:
|
||||||
|
return {"status": "ERROR", "reason": "Unknown card."}
|
||||||
|
|
||||||
|
if c != getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper():
|
||||||
|
print(c)
|
||||||
|
print(getSunMAC(bytes.fromhex(uid), bytes.fromhex(ctr)[::-1], bytes.fromhex(card.file_key)).hex().upper())
|
||||||
|
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||||
|
|
||||||
|
ctr_int = int(ctr, 16)
|
||||||
|
|
||||||
|
if ctr_int <= card.counter:
|
||||||
|
return {"status": "ERROR", "reason": "This link is already used."}
|
||||||
|
|
||||||
|
await update_card_counter(ctr_int, card.id)
|
||||||
|
|
||||||
|
link = await get_withdraw_link(card.withdraw, 0)
|
||||||
|
|
||||||
|
return link.lnurl_response(request)
|
||||||
|
|
||||||
|
@boltcards_ext.get("/api/v1/scane/")
|
||||||
|
async def api_scane(
|
||||||
|
e, c,
|
||||||
|
request: Request
|
||||||
|
):
|
||||||
|
card = None
|
||||||
|
counter = b''
|
||||||
|
|
||||||
|
for cand in await get_all_cards():
|
||||||
|
if cand.meta_key:
|
||||||
|
card_uid, counter = decryptSUN(bytes.fromhex(e), bytes.fromhex(cand.meta_key))
|
||||||
|
|
||||||
|
if card_uid.hex().upper() == cand.uid:
|
||||||
|
card = cand
|
||||||
|
break
|
||||||
|
|
||||||
|
if card == None:
|
||||||
|
return {"status": "ERROR", "reason": "Unknown card."}
|
||||||
|
|
||||||
|
if c != getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper():
|
||||||
|
print(c)
|
||||||
|
print(getSunMAC(card_uid, counter, bytes.fromhex(card.file_key)).hex().upper())
|
||||||
|
return {"status": "ERROR", "reason": "CMAC does not check."}
|
||||||
|
|
||||||
|
counter_int = int.from_bytes(counter, "little")
|
||||||
|
if counter_int <= card.counter:
|
||||||
|
return {"status": "ERROR", "reason": "This link is already used."}
|
||||||
|
|
||||||
|
await update_card_counter(counter_int, card.id)
|
||||||
|
|
||||||
|
link = await get_withdraw_link(card.withdraw, 0)
|
||||||
|
return link.lnurl_response(request)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue