commit
f13d5ec7ff
15 changed files with 1739 additions and 39 deletions
184
crud.py
184
crud.py
|
|
@ -1,10 +1,20 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
from .models import Merchant, PartialMerchant, PartialZone, Zone
|
from .models import (
|
||||||
|
Merchant,
|
||||||
|
PartialMerchant,
|
||||||
|
PartialProduct,
|
||||||
|
PartialStall,
|
||||||
|
PartialZone,
|
||||||
|
Product,
|
||||||
|
Stall,
|
||||||
|
Zone,
|
||||||
|
)
|
||||||
|
|
||||||
######################################## MERCHANT ########################################
|
######################################## MERCHANT ########################################
|
||||||
|
|
||||||
|
|
@ -51,15 +61,7 @@ async def create_zone(user_id: str, data: PartialZone) -> Zone:
|
||||||
zone_id = urlsafe_short_hash()
|
zone_id = urlsafe_short_hash()
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
INSERT INTO nostrmarket.zones (
|
INSERT INTO nostrmarket.zones (id, user_id, name, currency, cost, regions)
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
name,
|
|
||||||
currency,
|
|
||||||
cost,
|
|
||||||
regions
|
|
||||||
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
|
|
@ -104,4 +106,166 @@ async def get_zones(user_id: str) -> List[Zone]:
|
||||||
|
|
||||||
|
|
||||||
async def delete_zone(zone_id: str) -> None:
|
async def delete_zone(zone_id: str) -> None:
|
||||||
|
# todo: add user_id
|
||||||
await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,))
|
await db.execute("DELETE FROM nostrmarket.zones WHERE id = ?", (zone_id,))
|
||||||
|
|
||||||
|
|
||||||
|
######################################## STALL ########################################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_stall(user_id: str, data: PartialStall) -> Stall:
|
||||||
|
stall_id = urlsafe_short_hash()
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO nostrmarket.stalls (user_id, id, wallet, name, currency, zones, meta)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
stall_id,
|
||||||
|
data.wallet,
|
||||||
|
data.name,
|
||||||
|
data.currency,
|
||||||
|
json.dumps(
|
||||||
|
[z.dict() for z in data.shipping_zones]
|
||||||
|
), # todo: cost is float. should be int for sats
|
||||||
|
json.dumps(data.config.dict()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
stall = await get_stall(user_id, stall_id)
|
||||||
|
assert stall, "Newly created stall couldn't be retrieved"
|
||||||
|
return stall
|
||||||
|
|
||||||
|
|
||||||
|
async def get_stall(user_id: str, stall_id: str) -> Optional[Stall]:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM nostrmarket.stalls WHERE user_id = ? AND id = ?",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
stall_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Stall.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_stalls(user_id: str) -> List[Stall]:
|
||||||
|
rows = await db.fetchall(
|
||||||
|
"SELECT * FROM nostrmarket.stalls WHERE user_id = ?",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
return [Stall.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def update_stall(user_id: str, stall: Stall) -> Optional[Stall]:
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE nostrmarket.stalls SET wallet = ?, name = ?, currency = ?, zones = ?, meta = ?
|
||||||
|
WHERE user_id = ? AND id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
stall.wallet,
|
||||||
|
stall.name,
|
||||||
|
stall.currency,
|
||||||
|
json.dumps(
|
||||||
|
[z.dict() for z in stall.shipping_zones]
|
||||||
|
), # todo: cost is float. should be int for sats
|
||||||
|
json.dumps(stall.config.dict()),
|
||||||
|
user_id,
|
||||||
|
stall.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return await get_stall(user_id, stall.id)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_stall(user_id: str, stall_id: str) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"DELETE FROM nostrmarket.stalls WHERE user_id =? AND id = ?",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
stall_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
######################################## STALL ########################################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_product(user_id: str, data: PartialProduct) -> Product:
|
||||||
|
product_id = urlsafe_short_hash()
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO nostrmarket.products (user_id, id, stall_id, name, category_list, description, images, price, quantity)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
product_id,
|
||||||
|
data.stall_id,
|
||||||
|
data.name,
|
||||||
|
json.dumps(data.categories),
|
||||||
|
data.description,
|
||||||
|
data.image,
|
||||||
|
data.price,
|
||||||
|
data.quantity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
product = await get_product(user_id, product_id)
|
||||||
|
assert product, "Newly created product couldn't be retrieved"
|
||||||
|
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
async def update_product(user_id: str, product: Product) -> Product:
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE nostrmarket.products set name = ?, category_list = ?, description = ?, images = ?, price = ?, quantity = ?
|
||||||
|
WHERE user_id = ? AND id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
product.name,
|
||||||
|
json.dumps(product.categories),
|
||||||
|
product.description,
|
||||||
|
product.image,
|
||||||
|
product.price,
|
||||||
|
product.quantity,
|
||||||
|
user_id,
|
||||||
|
product.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
updated_product = await get_product(user_id, product.id)
|
||||||
|
assert updated_product, "Updated product couldn't be retrieved"
|
||||||
|
|
||||||
|
return updated_product
|
||||||
|
|
||||||
|
|
||||||
|
async def get_product(user_id: str, product_id: str) -> Optional[Product]:
|
||||||
|
row = await db.fetchone(
|
||||||
|
"SELECT * FROM nostrmarket.products WHERE user_id =? AND id = ?",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
product_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Product.from_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_products(user_id: str, stall_id: str) -> List[Product]:
|
||||||
|
rows = await db.fetchall(
|
||||||
|
"SELECT * FROM nostrmarket.products WHERE user_id = ? AND stall_id = ?",
|
||||||
|
(user_id, stall_id),
|
||||||
|
)
|
||||||
|
return [Product.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_product(user_id: str, product_id: str) -> None:
|
||||||
|
await db.execute(
|
||||||
|
"DELETE FROM nostrmarket.products WHERE user_id =? AND id = ?",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
product_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
||||||
81
helpers.py
Normal file
81
helpers.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import secp256k1
|
||||||
|
from cffi import FFI
|
||||||
|
from cryptography.hazmat.primitives import padding
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
|
||||||
|
|
||||||
|
def get_shared_secret(privkey: str, pubkey: str):
|
||||||
|
point = secp256k1.PublicKey(bytes.fromhex("02" + pubkey), True)
|
||||||
|
return point.ecdh(bytes.fromhex(privkey), hashfn=copy_x)
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_message(encoded_message: str, encryption_key) -> str:
|
||||||
|
encoded_data = encoded_message.split("?iv=")
|
||||||
|
encoded_content, encoded_iv = encoded_data[0], encoded_data[1]
|
||||||
|
|
||||||
|
iv = base64.b64decode(encoded_iv)
|
||||||
|
cipher = Cipher(algorithms.AES(encryption_key), modes.CBC(iv))
|
||||||
|
encrypted_content = base64.b64decode(encoded_content)
|
||||||
|
|
||||||
|
decryptor = cipher.decryptor()
|
||||||
|
decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize()
|
||||||
|
|
||||||
|
unpadder = padding.PKCS7(128).unpadder()
|
||||||
|
unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize()
|
||||||
|
|
||||||
|
return unpadded_data.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_message(message: str, encryption_key, iv: Optional[bytes]) -> str:
|
||||||
|
padder = padding.PKCS7(128).padder()
|
||||||
|
padded_data = padder.update(message.encode()) + padder.finalize()
|
||||||
|
|
||||||
|
iv = iv if iv else secrets.token_bytes(16)
|
||||||
|
cipher = Cipher(algorithms.AES(encryption_key), modes.CBC(iv))
|
||||||
|
|
||||||
|
encryptor = cipher.encryptor()
|
||||||
|
encrypted_message = encryptor.update(padded_data) + encryptor.finalize()
|
||||||
|
|
||||||
|
return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}"
|
||||||
|
|
||||||
|
|
||||||
|
def sign_message_hash(private_key: str, hash: bytes) -> str:
|
||||||
|
privkey = secp256k1.PrivateKey(bytes.fromhex(private_key))
|
||||||
|
sig = privkey.schnorr_sign(hash, None, raw=True)
|
||||||
|
return sig.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def test_decrypt_encrypt(encoded_message: str, encryption_key):
|
||||||
|
msg = decrypt_message(encoded_message, encryption_key)
|
||||||
|
|
||||||
|
# ecrypt using the same initialisation vector
|
||||||
|
iv = base64.b64decode(encoded_message.split("?iv=")[1])
|
||||||
|
ecrypted_msg = encrypt_message(msg, encryption_key, iv)
|
||||||
|
assert (
|
||||||
|
encoded_message == ecrypted_msg
|
||||||
|
), f"expected '{encoded_message}', but got '{ecrypted_msg}'"
|
||||||
|
print("### test_decrypt_encrypt", encoded_message == ecrypted_msg)
|
||||||
|
|
||||||
|
|
||||||
|
ffi = FFI()
|
||||||
|
|
||||||
|
|
||||||
|
@ffi.callback(
|
||||||
|
"int (unsigned char *, const unsigned char *, const unsigned char *, void *)"
|
||||||
|
)
|
||||||
|
def copy_x(output, x32, y32, data):
|
||||||
|
ffi.memmove(output, x32, 32)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def is_json(string: str):
|
||||||
|
try:
|
||||||
|
json.loads(string)
|
||||||
|
except ValueError as e:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
@ -18,15 +18,17 @@ async def m001_initial(db):
|
||||||
"""
|
"""
|
||||||
Initial stalls table.
|
Initial stalls table.
|
||||||
"""
|
"""
|
||||||
|
# user_id, id, wallet, name, currency, zones, meta
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE nostrmarket.stalls (
|
CREATE TABLE nostrmarket.stalls (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
currency TEXT,
|
currency TEXT,
|
||||||
shipping_zones TEXT NOT NULL,
|
zones TEXT NOT NULL DEFAULT '[]',
|
||||||
rating REAL DEFAULT 0
|
meta TEXT NOT NULL DEFAULT '{}'
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
@ -37,15 +39,15 @@ async def m001_initial(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
f"""
|
f"""
|
||||||
CREATE TABLE nostrmarket.products (
|
CREATE TABLE nostrmarket.products (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
stall_id TEXT NOT NULL,
|
stall_id TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
categories TEXT,
|
category_list TEXT DEFAULT '[]',
|
||||||
description TEXT,
|
description TEXT,
|
||||||
image TEXT,
|
images TEXT DEFAULT '[]',
|
||||||
price REAL NOT NULL,
|
price REAL NOT NULL,
|
||||||
quantity INTEGER NOT NULL,
|
quantity INTEGER NOT NULL
|
||||||
rating REAL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
||||||
139
models.py
139
models.py
|
|
@ -1,10 +1,13 @@
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import Query
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .helpers import sign_message_hash
|
||||||
|
from .nostr.event import NostrEvent
|
||||||
|
|
||||||
|
|
||||||
######################################## MERCHANT ########################################
|
######################################## MERCHANT ########################################
|
||||||
class MerchantConfig(BaseModel):
|
class MerchantConfig(BaseModel):
|
||||||
|
|
@ -20,6 +23,9 @@ class PartialMerchant(BaseModel):
|
||||||
class Merchant(PartialMerchant):
|
class Merchant(PartialMerchant):
|
||||||
id: str
|
id: str
|
||||||
|
|
||||||
|
def sign_hash(self, hash: bytes) -> str:
|
||||||
|
return sign_message_hash(self.private_key, hash)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_row(cls, row: Row) -> "Merchant":
|
def from_row(cls, row: Row) -> "Merchant":
|
||||||
merchant = cls(**dict(row))
|
merchant = cls(**dict(row))
|
||||||
|
|
@ -43,3 +49,134 @@ class Zone(PartialZone):
|
||||||
zone = cls(**dict(row))
|
zone = cls(**dict(row))
|
||||||
zone.countries = json.loads(row["regions"])
|
zone.countries = json.loads(row["regions"])
|
||||||
return zone
|
return zone
|
||||||
|
|
||||||
|
|
||||||
|
######################################## STALLS ########################################
|
||||||
|
|
||||||
|
|
||||||
|
class StallConfig(BaseModel):
|
||||||
|
"""Last published nostr event id for this Stall"""
|
||||||
|
|
||||||
|
event_id: Optional[str]
|
||||||
|
image_url: Optional[str]
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class PartialStall(BaseModel):
|
||||||
|
wallet: str
|
||||||
|
name: str
|
||||||
|
currency: str = "sat"
|
||||||
|
shipping_zones: List[Zone] = []
|
||||||
|
config: StallConfig = StallConfig()
|
||||||
|
|
||||||
|
def validate_stall(self):
|
||||||
|
for z in self.shipping_zones:
|
||||||
|
if z.currency != self.currency:
|
||||||
|
raise ValueError(
|
||||||
|
f"Sipping zone '{z.name}' has different currency than stall."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Stall(PartialStall):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||||
|
content = {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.config.description,
|
||||||
|
"currency": self.currency,
|
||||||
|
"shipping": [dict(z) for z in self.shipping_zones],
|
||||||
|
}
|
||||||
|
event = NostrEvent(
|
||||||
|
pubkey=pubkey,
|
||||||
|
created_at=round(time.time()),
|
||||||
|
kind=30005,
|
||||||
|
tags=[["d", self.id]],
|
||||||
|
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
||||||
|
)
|
||||||
|
event.id = event.event_id
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
def to_nostr_delete_event(self, pubkey: str) -> NostrEvent:
|
||||||
|
delete_event = NostrEvent(
|
||||||
|
pubkey=pubkey,
|
||||||
|
created_at=round(time.time()),
|
||||||
|
kind=5,
|
||||||
|
tags=[["e", self.config.event_id]],
|
||||||
|
content=f"Stall '{self.name}' deleted",
|
||||||
|
)
|
||||||
|
delete_event.id = delete_event.event_id
|
||||||
|
|
||||||
|
return delete_event
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Stall":
|
||||||
|
stall = cls(**dict(row))
|
||||||
|
stall.config = StallConfig(**json.loads(row["meta"]))
|
||||||
|
stall.shipping_zones = [Zone(**z) for z in json.loads(row["zones"])]
|
||||||
|
return stall
|
||||||
|
|
||||||
|
|
||||||
|
######################################## STALLS ########################################
|
||||||
|
|
||||||
|
|
||||||
|
class PartialProduct(BaseModel):
|
||||||
|
stall_id: str
|
||||||
|
name: str
|
||||||
|
categories: List[str] = []
|
||||||
|
description: Optional[str]
|
||||||
|
image: Optional[str]
|
||||||
|
price: float
|
||||||
|
quantity: int
|
||||||
|
|
||||||
|
def validate_product(self):
|
||||||
|
if self.image:
|
||||||
|
image_is_url = self.image.startswith("https://") or self.image.startswith(
|
||||||
|
"http://"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not image_is_url:
|
||||||
|
|
||||||
|
def size(b64string):
|
||||||
|
return int((len(b64string) * 3) / 4 - b64string.count("=", -2))
|
||||||
|
|
||||||
|
image_size = size(self.image) / 1024
|
||||||
|
if image_size > 100:
|
||||||
|
raise ValueError(
|
||||||
|
f"""
|
||||||
|
Image size is too big, {int(image_size)}Kb.
|
||||||
|
Max: 100kb, Compress the image at https://tinypng.com, or use an URL."""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Product(PartialProduct):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
def to_nostr_event(self, pubkey: str) -> NostrEvent:
|
||||||
|
content = {
|
||||||
|
"stall_id": self.stall_id,
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"image": self.image,
|
||||||
|
"price": self.price,
|
||||||
|
"quantity": self.quantity,
|
||||||
|
}
|
||||||
|
categories = [["t", tag] for tag in self.categories]
|
||||||
|
|
||||||
|
event = NostrEvent(
|
||||||
|
pubkey=pubkey,
|
||||||
|
created_at=round(time.time()),
|
||||||
|
kind=30005,
|
||||||
|
tags=[["d", self.id]] + categories,
|
||||||
|
content=json.dumps(content, separators=(",", ":"), ensure_ascii=False),
|
||||||
|
)
|
||||||
|
event.id = event.event_id
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "Product":
|
||||||
|
product = cls(**dict(row))
|
||||||
|
product.categories = json.loads(row["category_list"])
|
||||||
|
return product
|
||||||
|
|
|
||||||
57
nostr/event.py
Normal file
57
nostr/event.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from secp256k1 import PublicKey
|
||||||
|
|
||||||
|
|
||||||
|
class NostrEvent(BaseModel):
|
||||||
|
id: str = ""
|
||||||
|
pubkey: str
|
||||||
|
created_at: int
|
||||||
|
kind: int
|
||||||
|
tags: List[List[str]] = []
|
||||||
|
content: str = ""
|
||||||
|
sig: Optional[str]
|
||||||
|
|
||||||
|
def serialize(self) -> List:
|
||||||
|
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||||
|
|
||||||
|
def serialize_json(self) -> str:
|
||||||
|
e = self.serialize()
|
||||||
|
return json.dumps(e, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_id(self) -> str:
|
||||||
|
data = self.serialize_json()
|
||||||
|
id = hashlib.sha256(data.encode()).hexdigest()
|
||||||
|
return id
|
||||||
|
|
||||||
|
def check_signature(self):
|
||||||
|
event_id = self.event_id
|
||||||
|
if self.id != event_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid event id. Expected: '{event_id}' got '{self.id}'"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
pub_key = PublicKey(bytes.fromhex("02" + self.pubkey), True)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_signature = pub_key.schnorr_verify(
|
||||||
|
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
||||||
|
)
|
||||||
|
if not valid_signature:
|
||||||
|
raise ValueError(f"Invalid signature: '{self.sig}' for event '{self.id}'")
|
||||||
|
|
||||||
|
def stringify(self) -> str:
|
||||||
|
return json.dumps(dict(self))
|
||||||
|
|
||||||
|
def tag_values(self, tag_name: str) -> List[str]:
|
||||||
|
return [t[1] for t in self.tags if t[0] == tag_name]
|
||||||
|
|
||||||
|
def has_tag_value(self, tag_name: str, tag_value: str) -> bool:
|
||||||
|
return tag_value in self.tag_values(tag_name)
|
||||||
21
nostr/nostr_client.py
Normal file
21
nostr/nostr_client.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from lnbits.app import settings
|
||||||
|
from lnbits.helpers import url_for
|
||||||
|
|
||||||
|
from .event import NostrEvent
|
||||||
|
|
||||||
|
|
||||||
|
async def publish_nostr_event(e: NostrEvent):
|
||||||
|
url = url_for("/nostrclient/api/v1/publish", external=True)
|
||||||
|
data = dict(e)
|
||||||
|
print("### published", dict(data))
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
await client.post(
|
||||||
|
url,
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="public"
|
icon="public"
|
||||||
label="Shipping Zones"
|
|
||||||
@click="openZoneDialog()"
|
@click="openZoneDialog()"
|
||||||
>
|
>
|
||||||
<q-list>
|
<q-list>
|
||||||
|
|
@ -48,8 +47,8 @@
|
||||||
v-model="zoneDialog.data.countries"
|
v-model="zoneDialog.data.countries"
|
||||||
></q-select>
|
></q-select>
|
||||||
<q-select
|
<q-select
|
||||||
v-if="!zoneDialog.data.id"
|
:disabled="!!zoneDialog.data.id"
|
||||||
style="width: 100px"
|
:readonly="!!zoneDialog.data.id"
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
v-model="zoneDialog.data.currency"
|
v-model="zoneDialog.data.currency"
|
||||||
|
|
@ -60,7 +59,7 @@
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
:label="'Amount (' + zoneDialog.data.currency + ') *'"
|
:label="'Cost of Shipping (' + zoneDialog.data.currency + ') *'"
|
||||||
fill-mask="0"
|
fill-mask="0"
|
||||||
reverse-fill-mask
|
reverse-fill-mask
|
||||||
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
|
:step="zoneDialog.data.currency != 'sat' ? '0.01' : '1'"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
<div>
|
||||||
|
<q-tabs v-model="tab" no-caps class="bg-dark text-white shadow-2">
|
||||||
|
<q-tab name="info" label="Stall Info"></q-tab>
|
||||||
|
<q-tab name="products" label="Products"></q-tab>
|
||||||
|
<q-tab name="orders" label="Orders"></q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="tab">
|
||||||
|
<q-tab-panel name="info">
|
||||||
|
<div v-if="stall">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Name:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stall.name"
|
||||||
|
type="text"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Description:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stall.config.description"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Wallet:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="stall.wallet"
|
||||||
|
:options="walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Currency:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="stall.currency"
|
||||||
|
type="text"
|
||||||
|
label="Unit"
|
||||||
|
:options="currencies"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">Shipping Zones:</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg">
|
||||||
|
<q-select
|
||||||
|
:options="filteredZoneOptions"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
multiple
|
||||||
|
v-model.trim="stall.shipping_zones"
|
||||||
|
label="Shipping Zones"
|
||||||
|
></q-select>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row items-center q-mt-xl">
|
||||||
|
<div class="col-6 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="secondary"
|
||||||
|
class="float-left"
|
||||||
|
@click="updateStall()"
|
||||||
|
>Update Stall</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="pink"
|
||||||
|
icon="cancel"
|
||||||
|
class="float-right"
|
||||||
|
@click="deleteStall()"
|
||||||
|
>Delete Stall</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="products">
|
||||||
|
<div v-if="stall">
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-3 q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="green"
|
||||||
|
icon="plus"
|
||||||
|
class="float-left"
|
||||||
|
@click="showNewProductDialog()"
|
||||||
|
>New Product</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-8 q-pr-lg"></div>
|
||||||
|
<div class="col-3 col-sm-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col-12">
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="products"
|
||||||
|
row-key="id"
|
||||||
|
:columns="productsTable.columns"
|
||||||
|
:pagination.sync="productsTable.pagination"
|
||||||
|
:filter="productsFilter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="pink"
|
||||||
|
dense
|
||||||
|
@click="deleteProduct(props.row.id)"
|
||||||
|
icon="delete"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
dense
|
||||||
|
@click="editProduct(props.row)"
|
||||||
|
icon="edit"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="id" :props="props"> {{props.row.id}} </q-td>
|
||||||
|
<q-td key="name" :props="props"> {{props.row.name}} </q-td>
|
||||||
|
<q-td key="price" :props="props"> {{props.row.price}} </q-td>
|
||||||
|
<q-td key="quantity" :props="props">
|
||||||
|
{{props.row.quantity}}
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="categories" :props="props">
|
||||||
|
<div>
|
||||||
|
{{props.row.categories.filter(c => c).join(', ')}}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td key="description" :props="props">
|
||||||
|
{{props.row.description}}
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="orders">
|
||||||
|
<div v-if="stall"></div>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
<q-dialog v-model="productDialog.showDialog" position="top">
|
||||||
|
<q-card v-if="stall" class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="sendProductFormData" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.name"
|
||||||
|
label="Name"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.description"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
multiple
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model.trim="productDialog.data.categories"
|
||||||
|
use-input
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
hide-dropdown-icon
|
||||||
|
input-debounce="0"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
label="Categories (Hit Enter to add)"
|
||||||
|
placeholder="crafts,robots,etc"
|
||||||
|
></q-select>
|
||||||
|
<q-toggle
|
||||||
|
:label="`${productDialog.url ? 'Insert image URL' : 'Upload image file'}`"
|
||||||
|
v-model="productDialog.url"
|
||||||
|
></q-toggle>
|
||||||
|
<q-input
|
||||||
|
v-if="productDialog.url"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="productDialog.data.image"
|
||||||
|
type="url"
|
||||||
|
label="Image URL"
|
||||||
|
></q-input>
|
||||||
|
<q-file
|
||||||
|
v-else
|
||||||
|
class="q-pr-md"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
capture="environment"
|
||||||
|
accept="image/jpeg, image/png"
|
||||||
|
:max-file-size="3*1024**2"
|
||||||
|
label="Small image (optional)"
|
||||||
|
clearable
|
||||||
|
@input="imageAdded"
|
||||||
|
@clear="imageCleared"
|
||||||
|
>
|
||||||
|
<template v-if="productDialog.data.image" v-slot:before>
|
||||||
|
<img style="height: 1em" :src="productDialog.data.image" />
|
||||||
|
</template>
|
||||||
|
<template v-if="productDialog.data.image" v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
name="cancel"
|
||||||
|
@click.stop.prevent="imageCleared"
|
||||||
|
class="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="productDialog.data.price"
|
||||||
|
type="number"
|
||||||
|
:label="'Price (' + stall.currency + ') *'"
|
||||||
|
:step="stall.currency != 'sat' ? '0.01' : '1'"
|
||||||
|
:mask="stall.currency != 'sat' ? '#.##' : '#'"
|
||||||
|
fill-mask="0"
|
||||||
|
reverse-fill-mask
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.number="productDialog.data.quantity"
|
||||||
|
type="number"
|
||||||
|
label="Quantity"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
v-if="productDialog.data.id"
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>Update Product</q-btn
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!productDialog.data.price
|
||||||
|
|| !productDialog.data.name
|
||||||
|
|| !productDialog.data.quantity"
|
||||||
|
type="submit"
|
||||||
|
>Create Product</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>
|
||||||
|
|
@ -1,15 +1,309 @@
|
||||||
async function stallDetails(path) {
|
async function stallDetails(path) {
|
||||||
const template = await loadTemplateAsync(path)
|
const template = await loadTemplateAsync(path)
|
||||||
|
|
||||||
|
const pica = window.pica()
|
||||||
|
|
||||||
Vue.component('stall-details', {
|
Vue.component('stall-details', {
|
||||||
name: 'stall-details',
|
name: 'stall-details',
|
||||||
template,
|
template,
|
||||||
|
|
||||||
//props: ['stall-id', 'adminkey', 'inkey', 'wallet-options'],
|
props: [
|
||||||
|
'stall-id',
|
||||||
|
'adminkey',
|
||||||
|
'inkey',
|
||||||
|
'wallet-options',
|
||||||
|
'zone-options',
|
||||||
|
'currencies'
|
||||||
|
],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
tab: 'info',
|
tab: 'products',
|
||||||
relay: null
|
stall: null,
|
||||||
|
products: [],
|
||||||
|
productDialog: {
|
||||||
|
showDialog: false,
|
||||||
|
url: true,
|
||||||
|
data: {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
categories: [],
|
||||||
|
image: null,
|
||||||
|
price: 0,
|
||||||
|
quantity: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
productsFilter: '',
|
||||||
|
productsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'ID',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Price',
|
||||||
|
field: 'price'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quantity',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Quantity',
|
||||||
|
field: 'quantity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Categories',
|
||||||
|
field: 'categories'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Description',
|
||||||
|
field: 'description'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredZoneOptions: function () {
|
||||||
|
if (!this.stall) return []
|
||||||
|
return this.zoneOptions.filter(z => z.currency === this.stall.currency)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mapStall: function (stall) {
|
||||||
|
stall.shipping_zones.forEach(
|
||||||
|
z =>
|
||||||
|
(z.label = z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', '))
|
||||||
|
)
|
||||||
|
return stall
|
||||||
|
},
|
||||||
|
getStall: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.stall = this.mapStall(data)
|
||||||
|
|
||||||
|
console.log('### this.stall', this.stall)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateStall: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.adminkey,
|
||||||
|
this.stall
|
||||||
|
)
|
||||||
|
this.stall = this.mapStall(data)
|
||||||
|
this.$emit('stall-updated', this.stall)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall Updated',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteStall: function () {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog(
|
||||||
|
`
|
||||||
|
Products and orders will be deleted also!
|
||||||
|
Are you sure you want to delete this stall?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/stall/' + this.stallId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.$emit('stall-deleted', this.stallId)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall Deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
imageAdded(file) {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = URL.createObjectURL(file)
|
||||||
|
image.onload = async () => {
|
||||||
|
let fit = imgSizeFit(image)
|
||||||
|
let canvas = document.createElement('canvas')
|
||||||
|
canvas.setAttribute('width', fit.width)
|
||||||
|
canvas.setAttribute('height', fit.height)
|
||||||
|
output = await pica.resize(image, canvas)
|
||||||
|
this.productDialog.data.image = output.toDataURL('image/jpeg', 0.4)
|
||||||
|
this.productDialog = {...this.productDialog}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageCleared() {
|
||||||
|
this.productDialog.data.image = null
|
||||||
|
this.productDialog = {...this.productDialog}
|
||||||
|
},
|
||||||
|
getProducts: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/product/' + this.stall.id,
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.products = data
|
||||||
|
|
||||||
|
console.log('### this.products', this.products)
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendProductFormData: function () {
|
||||||
|
var data = {
|
||||||
|
stall_id: this.stall.id,
|
||||||
|
id: this.productDialog.data.id,
|
||||||
|
name: this.productDialog.data.name,
|
||||||
|
description: this.productDialog.data.description,
|
||||||
|
categories: this.productDialog.data.categories,
|
||||||
|
|
||||||
|
image: this.productDialog.data.image,
|
||||||
|
price: this.productDialog.data.price,
|
||||||
|
quantity: this.productDialog.data.quantity
|
||||||
|
}
|
||||||
|
this.productDialog.showDialog = false
|
||||||
|
if (this.productDialog.data.id) {
|
||||||
|
this.updateProduct(data)
|
||||||
|
} else {
|
||||||
|
this.createProduct(data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateProduct: async function (product) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'PATCH',
|
||||||
|
'/nostrmarket/api/v1/product/' + product.id,
|
||||||
|
this.adminkey,
|
||||||
|
product
|
||||||
|
)
|
||||||
|
const index = this.products.findIndex(r => r.id === product.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.products.splice(index, 1, data)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Updated',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createProduct: async function (payload) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/product',
|
||||||
|
this.adminkey,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
this.products.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product Created',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editProduct: async function (product) {
|
||||||
|
this.productDialog.data = {...product}
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
},
|
||||||
|
deleteProduct: async function (productId) {
|
||||||
|
LNbits.utils
|
||||||
|
.confirmDialog('Are you sure you want to delete this product?')
|
||||||
|
.onOk(async () => {
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostrmarket/api/v1/product/' + productId,
|
||||||
|
this.adminkey
|
||||||
|
)
|
||||||
|
this.products = _.reject(this.products, function (obj) {
|
||||||
|
return obj.id === productId
|
||||||
|
})
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Product deleted',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showNewProductDialog: async function () {
|
||||||
|
this.productDialog.data = {
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
categories: [],
|
||||||
|
image: null,
|
||||||
|
price: 0,
|
||||||
|
quantity: 0
|
||||||
|
}
|
||||||
|
this.productDialog.showDialog = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getStall()
|
||||||
|
await this.getProducts()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
149
static/components/stall-list/stall-list.html
Normal file
149
static/components/stall-list/stall-list.html
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
<div>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col q-pr-lg">
|
||||||
|
<q-btn
|
||||||
|
@click="openCreateStallDialog"
|
||||||
|
unelevated
|
||||||
|
color="green"
|
||||||
|
class="float-left"
|
||||||
|
>New Stall</q-btn
|
||||||
|
>
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="stalls"
|
||||||
|
row-key="id"
|
||||||
|
:columns="stallsTable.columns"
|
||||||
|
:pagination.sync="stallsTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
round
|
||||||
|
dense
|
||||||
|
@click="props.row.expanded= !props.row.expanded"
|
||||||
|
:icon="props.row.expanded? 'remove' : 'add'"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="id" :props="props">
|
||||||
|
<a style="color: unset" :href="props.row.id" target="_blank">
|
||||||
|
{{props.row.name}}</a
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
|
||||||
|
<q-td key="description" :props="props">
|
||||||
|
{{props.row.config.description}}
|
||||||
|
</q-td>
|
||||||
|
<q-td key="shippingZones" :props="props">
|
||||||
|
<div>
|
||||||
|
{{props.row.shipping_zones.filter(z => !!z.name).map(z =>
|
||||||
|
z.name).join(', ')}}
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
<q-tr v-if="props.row.expanded" :props="props">
|
||||||
|
<q-td colspan="100%">
|
||||||
|
<div class="row items-center q-mb-lg">
|
||||||
|
<div class="col-12">
|
||||||
|
<stall-details
|
||||||
|
:stall-id="props.row.id"
|
||||||
|
:adminkey="adminkey"
|
||||||
|
:inkey="inkey"
|
||||||
|
:wallet-options="walletOptions"
|
||||||
|
:zone-options="zoneOptions"
|
||||||
|
:currencies="currencies"
|
||||||
|
@stall-deleted="handleStallDeleted"
|
||||||
|
@stall-updated="handleStallUpdated"
|
||||||
|
></stall-details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<q-dialog v-model="stallDialog.show" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="sendStallFormData" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stallDialog.data.name"
|
||||||
|
label="Name"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="stallDialog.data.description"
|
||||||
|
type="textarea"
|
||||||
|
rows="3"
|
||||||
|
label="Description"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
emit-value
|
||||||
|
v-model="stallDialog.data.wallet"
|
||||||
|
:options="walletOptions"
|
||||||
|
label="Wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
<q-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="stallDialog.data.currency"
|
||||||
|
type="text"
|
||||||
|
label="Unit"
|
||||||
|
:options="currencies"
|
||||||
|
></q-select>
|
||||||
|
<q-select
|
||||||
|
:options="filteredZoneOptions"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
multiple
|
||||||
|
v-model.trim="stallDialog.data.shippingZones"
|
||||||
|
label="Shipping Zones"
|
||||||
|
></q-select>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!stallDialog.data.name
|
||||||
|
|| !stallDialog.data.currency
|
||||||
|
|| !stallDialog.data.wallet
|
||||||
|
|| !stallDialog.data.shippingZones
|
||||||
|
|| !stallDialog.data.shippingZones.length"
|
||||||
|
type="submit"
|
||||||
|
>Create Stall</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>
|
||||||
|
</div>
|
||||||
175
static/components/stall-list/stall-list.js
Normal file
175
static/components/stall-list/stall-list.js
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
async function stallList(path) {
|
||||||
|
const template = await loadTemplateAsync(path)
|
||||||
|
Vue.component('stall-list', {
|
||||||
|
name: 'stall-list',
|
||||||
|
template,
|
||||||
|
|
||||||
|
props: [`adminkey`, 'inkey', 'wallet-options'],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
filter: '',
|
||||||
|
stalls: [],
|
||||||
|
currencies: [],
|
||||||
|
stallDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
wallet: null,
|
||||||
|
currency: 'sat',
|
||||||
|
shippingZones: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoneOptions: [],
|
||||||
|
stallsTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Description',
|
||||||
|
field: 'description'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shippingZones',
|
||||||
|
align: 'left',
|
||||||
|
label: 'Shipping Zones',
|
||||||
|
field: 'shippingZones'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredZoneOptions: function () {
|
||||||
|
return this.zoneOptions.filter(
|
||||||
|
z => z.currency === this.stallDialog.data.currency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendStallFormData: async function () {
|
||||||
|
await this.createStall({
|
||||||
|
name: this.stallDialog.data.name,
|
||||||
|
wallet: this.stallDialog.data.wallet,
|
||||||
|
currency: this.stallDialog.data.currency,
|
||||||
|
shipping_zones: this.stallDialog.data.shippingZones,
|
||||||
|
config: {
|
||||||
|
description: this.stallDialog.data.description
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
createStall: async function (stall) {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
'/nostrmarket/api/v1/stall',
|
||||||
|
this.adminkey,
|
||||||
|
stall
|
||||||
|
)
|
||||||
|
this.stallDialog.show = false
|
||||||
|
data.expanded = false
|
||||||
|
this.stalls.unshift(data)
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Stall created!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCurrencies: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/currencies',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
|
||||||
|
this.currencies = ['sat', ...data]
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStalls: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/stall',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.stalls = data.map(s => ({...s, expanded: false}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getZones: async function () {
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
'/nostrmarket/api/v1/zone',
|
||||||
|
this.inkey
|
||||||
|
)
|
||||||
|
this.zoneOptions = data.map(z => ({
|
||||||
|
...z,
|
||||||
|
label: z.name
|
||||||
|
? `${z.name} (${z.countries.join(', ')})`
|
||||||
|
: z.countries.join(', ')
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleStallDeleted: function (stallId) {
|
||||||
|
this.stalls = _.reject(this.stalls, function (obj) {
|
||||||
|
return obj.id === stallId
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleStallUpdated: function (stall) {
|
||||||
|
const index = this.stalls.findIndex(r => r.id === stall.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
stall.expanded = true
|
||||||
|
this.stalls.splice(index, 1, stall)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreateStallDialog: async function () {
|
||||||
|
await this.getCurrencies()
|
||||||
|
await this.getZones()
|
||||||
|
if (!this.zoneOptions || !this.zoneOptions.length) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Please create a Shipping Zone first!'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.stallDialog.data = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
wallet: null,
|
||||||
|
currency: 'sat',
|
||||||
|
shippingZones: []
|
||||||
|
}
|
||||||
|
this.stallDialog.show = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: async function () {
|
||||||
|
await this.getStalls()
|
||||||
|
await this.getCurrencies()
|
||||||
|
await this.getZones()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
const merchant = async () => {
|
const merchant = async () => {
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
await stallDetails('static/components/stall-details/stall-details.html')
|
|
||||||
await keyPair('static/components/key-pair/key-pair.html')
|
await keyPair('static/components/key-pair/key-pair.html')
|
||||||
await shippingZones('static/components/shipping-zones/shipping-zones.html')
|
await shippingZones('static/components/shipping-zones/shipping-zones.html')
|
||||||
|
await stallDetails('static/components/stall-details/stall-details.html')
|
||||||
|
await stallList('static/components/stall-list/stall-list.html')
|
||||||
|
|
||||||
const nostr = window.NostrTools
|
const nostr = window.NostrTools
|
||||||
|
|
||||||
|
|
@ -14,16 +15,49 @@ const merchant = async () => {
|
||||||
return {
|
return {
|
||||||
merchant: {},
|
merchant: {},
|
||||||
shippingZones: [],
|
shippingZones: [],
|
||||||
showKeys: false
|
showKeys: false,
|
||||||
|
importKeyDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
privateKey: null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
generateKeys: async function () {
|
generateKeys: async function () {
|
||||||
const privkey = nostr.generatePrivateKey()
|
const privateKey = nostr.generatePrivateKey()
|
||||||
const pubkey = nostr.getPublicKey(privkey)
|
await this.createMerchant(privateKey)
|
||||||
|
},
|
||||||
const payload = {private_key: privkey, public_key: pubkey, config: {}}
|
importKeys: async function () {
|
||||||
|
this.importKeyDialog.show = false
|
||||||
|
let privateKey = this.importKeyDialog.data.privateKey
|
||||||
|
if (!privateKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
if (privateKey.toLowerCase().startsWith('nsec')) {
|
||||||
|
privateKey = nostr.nip19.decode(privateKey).data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: `${error}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await this.createMerchant(privateKey)
|
||||||
|
},
|
||||||
|
showImportKeysDialog: async function () {
|
||||||
|
this.importKeyDialog.show = true
|
||||||
|
},
|
||||||
|
createMerchant: async function (privateKey) {
|
||||||
|
try {
|
||||||
|
const pubkey = nostr.getPublicKey(privateKey)
|
||||||
|
const payload = {
|
||||||
|
private_key: privateKey,
|
||||||
|
public_key: pubkey,
|
||||||
|
config: {}
|
||||||
|
}
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'POST',
|
'POST',
|
||||||
'/nostrmarket/api/v1/merchant',
|
'/nostrmarket/api/v1/merchant',
|
||||||
|
|
@ -33,10 +67,13 @@ const merchant = async () => {
|
||||||
this.merchant = data
|
this.merchant = data
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Keys generated!'
|
message: 'Merchant Created!'
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
LNbits.utils.notifyApiError(error)
|
this.$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: `${error}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getMerchant: async function () {
|
getMerchant: async function () {
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,12 @@ function loadTemplateAsync(path) {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) {
|
||||||
|
let ratio = Math.min(
|
||||||
|
1,
|
||||||
|
maxWidth / img.naturalWidth,
|
||||||
|
maxHeight / img.naturalHeight
|
||||||
|
)
|
||||||
|
return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<q-btn
|
<q-btn
|
||||||
disabled
|
@click="showImportKeysDialog"
|
||||||
label="Import Key"
|
label="Import Key"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-left"
|
class="float-left"
|
||||||
|
|
@ -61,20 +61,22 @@
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6"></div>
|
<div class="col-8"></div>
|
||||||
<div class="col-4">
|
<div class="col-2">
|
||||||
<shipping-zones
|
<shipping-zones
|
||||||
:inkey="g.user.wallets[0].inkey"
|
:inkey="g.user.wallets[0].inkey"
|
||||||
:adminkey="g.user.wallets[0].adminkey"
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
|
class="float-right"
|
||||||
></shipping-zones>
|
></shipping-zones>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showKeys = !showKeys"
|
@click="showKeys = !showKeys"
|
||||||
:label="showKeys ? 'Hide Keys' : 'Show Keys'"
|
icon="vpn_key"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
class="float-right"
|
||||||
>
|
>
|
||||||
<q-tooltip> Show Public or Private keys </q-tooltip>
|
<q-tooltip> Show Public and Private keys </q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,6 +88,15 @@
|
||||||
></key-pair>
|
></key-pair>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
<q-card class="q-mt-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<stall-list
|
||||||
|
:adminkey="g.user.wallets[0].adminkey"
|
||||||
|
:inkey="g.user.wallets[0].inkey"
|
||||||
|
:wallet-options="g.user.walletOptions"
|
||||||
|
></stall-list>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -102,14 +113,43 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<q-dialog v-model="importKeyDialog.show" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
|
||||||
|
<q-form @submit="importKeys" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="importKeyDialog.data.privateKey"
|
||||||
|
label="Private Key (hex or nsec)"
|
||||||
|
></q-input>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!importKeyDialog.data.privateKey"
|
||||||
|
type="submit"
|
||||||
|
>Import</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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock%}{% block scripts %} {{ window_vars(user) }}
|
{% endblock%}{% block scripts %} {{ window_vars(user) }}
|
||||||
|
<!-- todo: serve locally -->
|
||||||
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
<script src="https://unpkg.com/nostr-tools/lib/nostr.bundle.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/pica@6.1.1/dist/pica.min.js"></script>
|
||||||
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/utils.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
|
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/key-pair/key-pair.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='components/key-pair/key-pair.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='components/shipping-zones/shipping-zones.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='components/shipping-zones/shipping-zones.js') }}"></script>
|
||||||
|
<script src="{{ url_for('nostrmarket_static', path='components/stall-details/stall-details.js') }}"></script>
|
||||||
|
<script src="{{ url_for('nostrmarket_static', path='components/stall-list/stall-list.js') }}"></script>
|
||||||
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
<script src="{{ url_for('nostrmarket_static', path='js/index.js') }}"></script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
246
views_api.py
246
views_api.py
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
@ -16,14 +17,33 @@ from lnbits.utils.exchange_rates import currencies
|
||||||
from . import nostrmarket_ext
|
from . import nostrmarket_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_merchant,
|
create_merchant,
|
||||||
|
create_product,
|
||||||
|
create_stall,
|
||||||
create_zone,
|
create_zone,
|
||||||
|
delete_product,
|
||||||
|
delete_stall,
|
||||||
delete_zone,
|
delete_zone,
|
||||||
get_merchant_for_user,
|
get_merchant_for_user,
|
||||||
|
get_products,
|
||||||
|
get_stall,
|
||||||
|
get_stalls,
|
||||||
get_zone,
|
get_zone,
|
||||||
get_zones,
|
get_zones,
|
||||||
|
update_product,
|
||||||
|
update_stall,
|
||||||
update_zone,
|
update_zone,
|
||||||
)
|
)
|
||||||
from .models import Merchant, PartialMerchant, PartialZone, Zone
|
from .models import (
|
||||||
|
Merchant,
|
||||||
|
PartialMerchant,
|
||||||
|
PartialProduct,
|
||||||
|
PartialStall,
|
||||||
|
PartialZone,
|
||||||
|
Product,
|
||||||
|
Stall,
|
||||||
|
Zone,
|
||||||
|
)
|
||||||
|
from .nostr.nostr_client import publish_nostr_event
|
||||||
|
|
||||||
######################################## MERCHANT ########################################
|
######################################## MERCHANT ########################################
|
||||||
|
|
||||||
|
|
@ -138,6 +158,230 @@ async def api_delete_zone(zone_id, wallet: WalletTypeInfo = Depends(require_admi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
######################################## STALLS ########################################
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.post("/api/v1/stall")
|
||||||
|
async def api_create_stall(
|
||||||
|
data: PartialStall,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Stall:
|
||||||
|
try:
|
||||||
|
data.validate_stall()
|
||||||
|
|
||||||
|
print("### stall", json.dumps(data.dict()))
|
||||||
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
assert merchant, "Cannot find merchat for stall"
|
||||||
|
|
||||||
|
stall = await create_stall(wallet.wallet.user, data=data)
|
||||||
|
|
||||||
|
event = stall.to_nostr_event(merchant.public_key)
|
||||||
|
event.sig = merchant.sign_hash(bytes.fromhex(event.id))
|
||||||
|
await publish_nostr_event(event)
|
||||||
|
|
||||||
|
stall.config.event_id = event.id
|
||||||
|
await update_stall(wallet.wallet.user, stall)
|
||||||
|
|
||||||
|
return stall
|
||||||
|
except ValueError as ex:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=str(ex),
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot create stall",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.put("/api/v1/stall/{stall_id}")
|
||||||
|
async def api_update_stall(
|
||||||
|
data: Stall,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Stall:
|
||||||
|
try:
|
||||||
|
data.validate_stall()
|
||||||
|
|
||||||
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
assert merchant, "Cannot find merchat for stall"
|
||||||
|
|
||||||
|
event = data.to_nostr_event(merchant.public_key)
|
||||||
|
event.sig = merchant.sign_hash(bytes.fromhex(event.id))
|
||||||
|
|
||||||
|
data.config.event_id = event.id
|
||||||
|
# data.config.event_created_at =
|
||||||
|
stall = await update_stall(wallet.wallet.user, data)
|
||||||
|
assert stall, "Cannot update stall"
|
||||||
|
|
||||||
|
await publish_nostr_event(event)
|
||||||
|
|
||||||
|
return stall
|
||||||
|
except HTTPException as ex:
|
||||||
|
raise ex
|
||||||
|
except ValueError as ex:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=str(ex),
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot update stall",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.get("/api/v1/stall/{stall_id}")
|
||||||
|
async def api_get_stall(stall_id: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
try:
|
||||||
|
stall = await get_stall(wallet.wallet.user, stall_id)
|
||||||
|
if not stall:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Stall does not exist.",
|
||||||
|
)
|
||||||
|
return stall
|
||||||
|
except HTTPException as ex:
|
||||||
|
raise ex
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot get stall",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.get("/api/v1/stall")
|
||||||
|
async def api_get_stalls(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
|
try:
|
||||||
|
stalls = await get_stalls(wallet.wallet.user)
|
||||||
|
return stalls
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot get stalls",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.delete("/api/v1/stall/{stall_id}")
|
||||||
|
async def api_delete_stall(
|
||||||
|
stall_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
stall = await get_stall(wallet.wallet.user, stall_id)
|
||||||
|
if not stall:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Stall does not exist.",
|
||||||
|
)
|
||||||
|
|
||||||
|
merchant = await get_merchant_for_user(wallet.wallet.user)
|
||||||
|
assert merchant, "Cannot find merchat for stall"
|
||||||
|
|
||||||
|
await delete_stall(wallet.wallet.user, stall_id)
|
||||||
|
|
||||||
|
delete_event = stall.to_nostr_delete_event(merchant.public_key)
|
||||||
|
delete_event.sig = merchant.sign_hash(bytes.fromhex(delete_event.id))
|
||||||
|
|
||||||
|
await publish_nostr_event(delete_event)
|
||||||
|
|
||||||
|
except HTTPException as ex:
|
||||||
|
raise ex
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot delete stall",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
######################################## PRODUCTS ########################################
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.post("/api/v1/product")
|
||||||
|
async def api_create_product(
|
||||||
|
data: PartialProduct,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Product:
|
||||||
|
try:
|
||||||
|
data.validate_product()
|
||||||
|
product = await create_product(wallet.wallet.user, data=data)
|
||||||
|
|
||||||
|
return product
|
||||||
|
except ValueError as ex:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=str(ex),
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot create product",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.patch("/api/v1/product/{product_id}")
|
||||||
|
async def api_update_product(
|
||||||
|
product_id: str,
|
||||||
|
product: Product,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> Product:
|
||||||
|
try:
|
||||||
|
product.validate_product()
|
||||||
|
product = await update_product(wallet.wallet.user, product)
|
||||||
|
|
||||||
|
return product
|
||||||
|
except ValueError as ex:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=str(ex),
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot update product",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.get("/api/v1/product/{stall_id}")
|
||||||
|
async def api_get_product(
|
||||||
|
stall_id: str,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
products = await get_products(wallet.wallet.user, stall_id)
|
||||||
|
return products
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot get product",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrmarket_ext.delete("/api/v1/product/{product_id}")
|
||||||
|
async def api_delete_product(
|
||||||
|
product_id: str,
|
||||||
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await delete_product(wallet.wallet.user, product_id)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot delete product",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
######################################## OTHER ########################################
|
||||||
|
|
||||||
|
|
||||||
@nostrmarket_ext.get("/api/v1/currencies")
|
@nostrmarket_ext.get("/api/v1/currencies")
|
||||||
async def api_list_currencies_available():
|
async def api_list_currencies_available():
|
||||||
return list(currencies.keys())
|
return list(currencies.keys())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue