mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Add RobotTokenSHA256 middleware, /api/robot and frontend entropy calc (#512)
* Add RobotTokenSHA256 middleware for in-the-fly robot generation/login * Add RobotView, fix middleware, upgrade frontend * Token header as base91 * Add OAS schema of RobotView * Use RobotView on new fetchRobot(), mimick old fetchRobot() functionality * Upgrade websockets for token based authentication * Small fixes * Add frontend token entropy checks, add token on route /robot/<token> * Rename admin panel * Collect phrases
This commit is contained in:
parent
b47e9565a8
commit
e6ddcf9e4b
@ -1,3 +1,6 @@
|
||||
# Coordinator Alias (Same as longAlias)
|
||||
COORDINATOR_ALIAS="Local Dev"
|
||||
|
||||
# LND directory to read TLS cert and macaroon
|
||||
LND_DIR='/lnd/'
|
||||
MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
|
||||
|
@ -2,7 +2,6 @@ import ast
|
||||
import math
|
||||
from datetime import timedelta
|
||||
|
||||
import gnupg
|
||||
from decouple import config
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q, Sum
|
||||
@ -88,56 +87,6 @@ class Logics:
|
||||
|
||||
return True, None, None
|
||||
|
||||
def validate_pgp_keys(pub_key, enc_priv_key):
|
||||
"""Validates PGP valid keys. Formats them in a way understandable by the frontend"""
|
||||
gpg = gnupg.GPG()
|
||||
|
||||
# Standarize format with linux linebreaks '\n'. Windows users submitting their own keys have '\r\n' breaking communication.
|
||||
enc_priv_key = enc_priv_key.replace("\r\n", "\n")
|
||||
pub_key = pub_key.replace("\r\n", "\n")
|
||||
|
||||
# Try to import the public key
|
||||
import_pub_result = gpg.import_keys(pub_key)
|
||||
if not import_pub_result.imported == 1:
|
||||
# If a robot is deleted and it is rebuilt with the same pubKey, the key will not be imported again
|
||||
# so we assert that the import error is "Not actually changed"
|
||||
if "Not actually changed" not in import_pub_result.results[0]["text"]:
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"bad_request": "Your PGP public key does not seem valid.\n"
|
||||
+ f"Stderr: {str(import_pub_result.stderr)}\n"
|
||||
+ f"ReturnCode: {str(import_pub_result.returncode)}\n"
|
||||
+ f"Summary: {str(import_pub_result.summary)}\n"
|
||||
+ f"Results: {str(import_pub_result.results)}\n"
|
||||
+ f"Imported: {str(import_pub_result.imported)}\n"
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
# Exports the public key again for uniform formatting.
|
||||
pub_key = gpg.export_keys(import_pub_result.fingerprints[0])
|
||||
|
||||
# Try to import the encrypted private key (without passphrase)
|
||||
import_priv_result = gpg.import_keys(enc_priv_key)
|
||||
if not import_priv_result.sec_imported == 1:
|
||||
if "Not actually changed" not in import_priv_result.results[0]["text"]:
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"bad_request": "Your PGP encrypted private key does not seem valid.\n"
|
||||
+ f"Stderr: {str(import_priv_result.stderr)}\n"
|
||||
+ f"ReturnCode: {str(import_priv_result.returncode)}\n"
|
||||
+ f"Summary: {str(import_priv_result.summary)}\n"
|
||||
+ f"Results: {str(import_priv_result.results)}\n"
|
||||
+ f"Sec Imported: {str(import_priv_result.sec_imported)}\n"
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
return True, None, pub_key, enc_priv_key
|
||||
|
||||
@classmethod
|
||||
def validate_order_size(cls, order):
|
||||
"""Validates if order size in Sats is within limits at t0"""
|
||||
|
@ -11014,7 +11014,7 @@ nouns = [
|
||||
"Zostera",
|
||||
"Zosterops",
|
||||
"Zouave",
|
||||
"Zu/is",
|
||||
"Zuis",
|
||||
"Zubr",
|
||||
"Zucchini",
|
||||
"Zuche",
|
||||
|
@ -467,6 +467,23 @@ class UserViewSchema:
|
||||
"description": "Whether the user prefers stealth invoices",
|
||||
},
|
||||
"found": {"type": "string", "description": "Welcome back message"},
|
||||
"tg_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The robot has telegram notifications enabled",
|
||||
},
|
||||
"tg_token": {
|
||||
"type": "string",
|
||||
"description": "Token to enable telegram with /start <tg_token>",
|
||||
},
|
||||
"tg_bot_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the coordinator's telegram bot",
|
||||
},
|
||||
"last_login": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "Last time seen",
|
||||
},
|
||||
"active_order_id": {
|
||||
"type": "integer",
|
||||
"description": "Active order id if present",
|
||||
@ -623,6 +640,114 @@ class BookViewSchema:
|
||||
}
|
||||
|
||||
|
||||
class RobotViewSchema:
|
||||
get = {
|
||||
"summary": "Get robot info",
|
||||
"description": textwrap.dedent(
|
||||
"""
|
||||
DEPRECATED: Use `/robot` GET.
|
||||
|
||||
Get robot info 🤖
|
||||
|
||||
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
||||
returned the information about the state of a robot.
|
||||
|
||||
Make sure you generate your token using cryptographically secure methods. [Here's]() the function the Javascript
|
||||
client uses to generate the tokens. Since the server only receives the hash of the
|
||||
token, it is responsibility of the client to create a strong token. Check
|
||||
[here](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/utils/token.js)
|
||||
to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens
|
||||
created by the user at will.
|
||||
|
||||
`public_key` - PGP key associated with the user (Armored ASCII format)
|
||||
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
|
||||
the frontend and the key can't really be used by the server since it's protected by the token
|
||||
that only the client knows. Will be made an optional parameter in a future release.
|
||||
On the Javascript client, It's passphrase is set to be the secret token generated.
|
||||
|
||||
A gpg key can be created by:
|
||||
|
||||
```shell
|
||||
gpg --full-gen-key
|
||||
```
|
||||
|
||||
it's public key can be exported in ascii armored format with:
|
||||
|
||||
```shell
|
||||
gpg --export --armor <key-id | email | name>
|
||||
```
|
||||
|
||||
and it's private key can be exported in ascii armored format with:
|
||||
|
||||
```shell
|
||||
gpg --export-secret-keys --armor <key-id | email | name>
|
||||
```
|
||||
|
||||
"""
|
||||
),
|
||||
"responses": {
|
||||
200: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encrypted_private_key": {
|
||||
"type": "string",
|
||||
"description": "Armored ASCII PGP private key block",
|
||||
},
|
||||
"nickname": {
|
||||
"type": "string",
|
||||
"description": "Username generated (Robot name)",
|
||||
},
|
||||
"public_key": {
|
||||
"type": "string",
|
||||
"description": "Armored ASCII PGP public key block",
|
||||
},
|
||||
"wants_stealth": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Whether the user prefers stealth invoices",
|
||||
},
|
||||
"found": {
|
||||
"type": "boolean",
|
||||
"description": "Robot had been created in the past. Only if the robot was created +5 mins ago.",
|
||||
},
|
||||
"tg_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The robot has telegram notifications enabled",
|
||||
},
|
||||
"tg_token": {
|
||||
"type": "string",
|
||||
"description": "Token to enable telegram with /start <tg_token>",
|
||||
},
|
||||
"tg_bot_name": {
|
||||
"type": "string",
|
||||
"description": "Name of the coordinator's telegram bot",
|
||||
},
|
||||
"active_order_id": {
|
||||
"type": "integer",
|
||||
"description": "Active order id if present",
|
||||
},
|
||||
"last_order_id": {
|
||||
"type": "integer",
|
||||
"description": "Last order id if present",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"examples": [
|
||||
OpenApiExample(
|
||||
"Successfully retrieved robot",
|
||||
value={
|
||||
"nickname": "SatoshiNakamoto21",
|
||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n......\n......",
|
||||
"encrypted_private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n......\n......",
|
||||
"wants_stealth": True,
|
||||
},
|
||||
status_codes=[200],
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class InfoViewSchema:
|
||||
get = {
|
||||
"summary": "Get info",
|
||||
|
@ -12,6 +12,7 @@ from .views import (
|
||||
OrderView,
|
||||
PriceView,
|
||||
RewardView,
|
||||
RobotView,
|
||||
StealthView,
|
||||
TickView,
|
||||
UserView,
|
||||
@ -26,6 +27,7 @@ urlpatterns = [
|
||||
OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
|
||||
),
|
||||
path("user/", UserView.as_view()),
|
||||
path("robot/", RobotView.as_view()),
|
||||
path("book/", BookView.as_view()),
|
||||
path("info/", InfoView.as_view()),
|
||||
path("price/", PriceView.as_view()),
|
||||
|
86
api/utils.py
86
api/utils.py
@ -2,9 +2,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import gnupg
|
||||
import numpy as np
|
||||
import requests
|
||||
import ring
|
||||
from base91 import decode, encode
|
||||
from decouple import config
|
||||
|
||||
from api.models import Order
|
||||
@ -147,18 +149,6 @@ def get_robosats_commit():
|
||||
return commit_hash
|
||||
|
||||
|
||||
robosats_version_cache = {}
|
||||
|
||||
|
||||
@ring.dict(robosats_commit_cache, expire=99999)
|
||||
def get_robosats_version():
|
||||
|
||||
with open("version.json") as f:
|
||||
version_dict = json.load(f)
|
||||
|
||||
return version_dict
|
||||
|
||||
|
||||
premium_percentile = {}
|
||||
|
||||
|
||||
@ -238,3 +228,75 @@ def compute_avg_premium(queryset):
|
||||
else:
|
||||
weighted_median_premium = 0.0
|
||||
return weighted_median_premium, total_volume
|
||||
|
||||
|
||||
def validate_pgp_keys(pub_key, enc_priv_key):
|
||||
"""Validates PGP valid keys. Formats them in a way understandable by the frontend"""
|
||||
gpg = gnupg.GPG()
|
||||
|
||||
# Standardize format with linux linebreaks '\n'. Windows users submitting their own keys have '\r\n' breaking communication.
|
||||
enc_priv_key = enc_priv_key.replace("\r\n", "\n").replace("\\", "\n")
|
||||
pub_key = pub_key.replace("\r\n", "\n").replace("\\", "\n")
|
||||
|
||||
# Try to import the public key
|
||||
import_pub_result = gpg.import_keys(pub_key)
|
||||
if not import_pub_result.imported == 1:
|
||||
# If a robot is deleted and it is rebuilt with the same pubKey, the key will not be imported again
|
||||
# so we assert that the import error is "Not actually changed"
|
||||
if "Not actually changed" not in import_pub_result.results[0]["text"]:
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"bad_request": "Your PGP public key does not seem valid.\n"
|
||||
+ f"Stderr: {str(import_pub_result.stderr)}\n"
|
||||
+ f"ReturnCode: {str(import_pub_result.returncode)}\n"
|
||||
+ f"Summary: {str(import_pub_result.summary)}\n"
|
||||
+ f"Results: {str(import_pub_result.results)}\n"
|
||||
+ f"Imported: {str(import_pub_result.imported)}\n"
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
# Exports the public key again for uniform formatting.
|
||||
pub_key = gpg.export_keys(import_pub_result.fingerprints[0])
|
||||
|
||||
# Try to import the encrypted private key (without passphrase)
|
||||
import_priv_result = gpg.import_keys(enc_priv_key)
|
||||
if not import_priv_result.sec_imported == 1:
|
||||
if "Not actually changed" not in import_priv_result.results[0]["text"]:
|
||||
return (
|
||||
False,
|
||||
{
|
||||
"bad_request": "Your PGP encrypted private key does not seem valid.\n"
|
||||
+ f"Stderr: {str(import_priv_result.stderr)}\n"
|
||||
+ f"ReturnCode: {str(import_priv_result.returncode)}\n"
|
||||
+ f"Summary: {str(import_priv_result.summary)}\n"
|
||||
+ f"Results: {str(import_priv_result.results)}\n"
|
||||
+ f"Sec Imported: {str(import_priv_result.sec_imported)}\n"
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
return True, None, pub_key, enc_priv_key
|
||||
|
||||
|
||||
def base91_to_hex(base91_str: str) -> str:
|
||||
bytes_data = decode(base91_str)
|
||||
return bytes_data.hex()
|
||||
|
||||
|
||||
def hex_to_base91(hex_str: str) -> str:
|
||||
hex_bytes = bytes.fromhex(hex_str)
|
||||
base91_str = encode(hex_bytes)
|
||||
return base91_str
|
||||
|
||||
|
||||
def is_valid_token(token: str) -> bool:
|
||||
num_chars = len(token)
|
||||
|
||||
if not 38 < num_chars < 41:
|
||||
return False
|
||||
|
||||
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"'
|
||||
return all(c in charset for c in token)
|
||||
|
81
api/views.py
81
api/views.py
@ -12,7 +12,12 @@ from django.db.models import Q, Sum
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.authentication import (
|
||||
SessionAuthentication, # DEPRECATE session authentication
|
||||
)
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.generics import CreateAPIView, ListAPIView, UpdateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from robohash import Robohash
|
||||
@ -30,6 +35,7 @@ from api.oas_schemas import (
|
||||
OrderViewSchema,
|
||||
PriceViewSchema,
|
||||
RewardViewSchema,
|
||||
RobotViewSchema,
|
||||
StealthViewSchema,
|
||||
TickViewSchema,
|
||||
UserViewSchema,
|
||||
@ -51,7 +57,7 @@ from api.utils import (
|
||||
compute_premium_percentile,
|
||||
get_lnd_version,
|
||||
get_robosats_commit,
|
||||
get_robosats_version,
|
||||
validate_pgp_keys,
|
||||
)
|
||||
from chat.models import Message
|
||||
from control.models import AccountingDay, BalanceLog
|
||||
@ -72,9 +78,12 @@ avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class MakerView(CreateAPIView):
|
||||
serializer_class = MakeOrderSerializer
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(**MakerViewSchema.post)
|
||||
def post(self, request):
|
||||
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
@ -178,6 +187,8 @@ class MakerView(CreateAPIView):
|
||||
|
||||
|
||||
class OrderView(viewsets.ViewSet):
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = UpdateOrderSerializer
|
||||
lookup_url_kwarg = "order_id"
|
||||
|
||||
@ -617,13 +628,60 @@ class OrderView(viewsets.ViewSet):
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class RobotView(APIView):
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(**RobotViewSchema.get)
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
Respond with Nickname, pubKey, privKey.
|
||||
"""
|
||||
user = request.user
|
||||
context = {}
|
||||
context["nickname"] = user.username
|
||||
context["public_key"] = user.robot.public_key
|
||||
context["encrypted_private_key"] = user.robot.encrypted_private_key
|
||||
context["earned_rewards"] = user.robot.earned_rewards
|
||||
context["wants_stealth"] = user.robot.wants_stealth
|
||||
context["last_login"] = user.last_login
|
||||
|
||||
# Adds/generate telegram token and whether it is enabled
|
||||
context = {**context, **Telegram.get_context(user)}
|
||||
|
||||
# return active order or last made order if any
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user
|
||||
)
|
||||
if not has_no_active_order:
|
||||
context["active_order_id"] = order.id
|
||||
else:
|
||||
last_order = Order.objects.filter(
|
||||
Q(maker=request.user) | Q(taker=request.user)
|
||||
).last()
|
||||
if last_order:
|
||||
context["last_order_id"] = last_order.id
|
||||
|
||||
# Robot was found, only if created +5 mins ago
|
||||
if user.date_joined < (timezone.now() - timedelta(minutes=5)):
|
||||
context["found"] = True
|
||||
|
||||
return Response(context, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class UserView(APIView):
|
||||
"""
|
||||
Deprecated. UserView will be completely replaced by the smaller RobotView in
|
||||
combination with the RobotTokenSHA256 middleware (on-the-fly robot generation)
|
||||
"""
|
||||
|
||||
NickGen = NickGenerator(
|
||||
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
||||
)
|
||||
|
||||
serializer_class = UserGenSerializer
|
||||
|
||||
@extend_schema(**UserViewSchema.post)
|
||||
def post(self, request, format=None):
|
||||
"""
|
||||
Get a new user derived from a high entropy token
|
||||
@ -715,7 +773,7 @@ class UserView(APIView):
|
||||
bad_keys_context,
|
||||
public_key,
|
||||
encrypted_private_key,
|
||||
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
||||
) = validate_pgp_keys(public_key, encrypted_private_key)
|
||||
if not valid:
|
||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -922,7 +980,7 @@ class InfoView(ListAPIView):
|
||||
context["lifetime_volume"] = round(lifetime_volume, 8)
|
||||
context["lnd_version"] = get_lnd_version()
|
||||
context["robosats_running_commit_hash"] = get_robosats_commit()
|
||||
context["version"] = get_robosats_version()
|
||||
context["version"] = settings.VERSION
|
||||
context["alternative_site"] = config("ALTERNATIVE_SITE")
|
||||
context["alternative_name"] = config("ALTERNATIVE_NAME")
|
||||
context["node_alias"] = config("NODE_ALIAS")
|
||||
@ -945,18 +1003,15 @@ class InfoView(ListAPIView):
|
||||
|
||||
|
||||
class RewardView(CreateAPIView):
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
serializer_class = ClaimRewardSerializer
|
||||
|
||||
@extend_schema(**RewardViewSchema.post)
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{"bad_request": "Woops! It seems you do not have a robot avatar"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -1052,6 +1107,8 @@ class HistoricalView(ListAPIView):
|
||||
|
||||
|
||||
class StealthView(UpdateAPIView):
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
serializer_class = StealthSerializer
|
||||
|
||||
@ -1059,12 +1116,6 @@ class StealthView(UpdateAPIView):
|
||||
def put(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{"bad_request": "Woops! It seems you do not have a robot avatar"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
@ -5,6 +5,11 @@ from channels.layers import get_channel_layer
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.authentication import (
|
||||
SessionAuthentication, # DEPRECATE session Authentication
|
||||
)
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from api.models import Order
|
||||
@ -15,6 +20,9 @@ from chat.serializers import ChatSerializer, PostMessageSerializer
|
||||
|
||||
class ChatView(viewsets.ViewSet):
|
||||
serializer_class = PostMessageSerializer
|
||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
lookup_url_kwarg = ["order_id", "offset"]
|
||||
|
||||
queryset = Message.objects.filter(
|
||||
|
@ -1,3 +0,0 @@
|
||||
# from django.db import models
|
||||
|
||||
# Create your models here.
|
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@ -21,6 +21,7 @@
|
||||
"@mui/x-date-pickers": "^6.0.4",
|
||||
"@nivo/core": "^0.80.0",
|
||||
"@nivo/line": "^0.80.0",
|
||||
"base-ex": "^0.7.5",
|
||||
"country-flag-icons": "^1.4.25",
|
||||
"date-fns": "^2.28.0",
|
||||
"file-replace-loader": "^1.4.0",
|
||||
@ -5165,6 +5166,11 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/base-ex": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/base-ex/-/base-ex-0.7.5.tgz",
|
||||
"integrity": "sha512-tJhPMqiXc6GTrMZusgZIJvYV9wFZ5lReDUsmm4AIIznAFm2ZD8i1/bAlgTkkR2elxjdtHAGFoKaALXDaJBv4yA=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@ -18920,6 +18926,11 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"base-ex": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/base-ex/-/base-ex-0.7.5.tgz",
|
||||
"integrity": "sha512-tJhPMqiXc6GTrMZusgZIJvYV9wFZ5lReDUsmm4AIIznAFm2ZD8i1/bAlgTkkR2elxjdtHAGFoKaALXDaJBv4yA=="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
|
@ -59,6 +59,7 @@
|
||||
"@mui/x-date-pickers": "^6.0.4",
|
||||
"@nivo/core": "^0.80.0",
|
||||
"@nivo/line": "^0.80.0",
|
||||
"base-ex": "^0.7.5",
|
||||
"country-flag-icons": "^1.4.25",
|
||||
"date-fns": "^2.28.0",
|
||||
"file-replace-loader": "^1.4.0",
|
||||
|
@ -80,7 +80,6 @@ const BookPage = (): JSX.Element => {
|
||||
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
|
||||
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
||||
<MakerForm
|
||||
hasRobot={robot.avatarLoaded}
|
||||
onOrderCreated={(id) => {
|
||||
navigate('/order/' + id);
|
||||
}}
|
||||
|
@ -77,7 +77,7 @@ const Main: React.FC = () => {
|
||||
|
||||
<MainBox navbarHeight={navbarHeight}>
|
||||
<Routes>
|
||||
{['/robot/:refCode?', '/', ''].map((path, index) => {
|
||||
{['/robot/:token?', '/', ''].map((path, index) => {
|
||||
return (
|
||||
<Route
|
||||
path={path}
|
||||
|
@ -100,7 +100,6 @@ const MakerPage = (): JSX.Element => {
|
||||
onOrderCreated={(id) => {
|
||||
navigate('/order/' + id);
|
||||
}}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
disableRequest={matches.length > 0 && !showMatches}
|
||||
collapseAll={showMatches}
|
||||
onSubmit={() => setShowMatches(matches.length > 0)}
|
||||
|
@ -58,7 +58,7 @@ const OrderPage = (): JSX.Element => {
|
||||
escrow_duration: order.escrow_duration,
|
||||
bond_size: order.bond_size,
|
||||
};
|
||||
apiClient.post(baseUrl, '/api/make/', body).then((data: any) => {
|
||||
apiClient.post(baseUrl, '/api/make/', body, robot.tokenSHA256).then((data: any) => {
|
||||
if (data.bad_request) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.id) {
|
||||
|
@ -11,12 +11,10 @@ import {
|
||||
LinearProgress,
|
||||
Link,
|
||||
Typography,
|
||||
useTheme,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import { Page } from '../NavBar';
|
||||
import { Robot } from '../../models';
|
||||
import { Casino, Bolt, Check, Storefront, AddBox, School } from '@mui/icons-material';
|
||||
import RobotAvatar from '../../components/RobotAvatar';
|
||||
@ -31,7 +29,7 @@ interface OnboardingProps {
|
||||
inputToken: string;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
badRequest: string | undefined;
|
||||
badToken: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
@ -41,7 +39,7 @@ const Onboarding = ({
|
||||
inputToken,
|
||||
setInputToken,
|
||||
setRobot,
|
||||
badRequest,
|
||||
badToken,
|
||||
getGenerateRobot,
|
||||
baseUrl,
|
||||
}: OnboardingProps): JSX.Element => {
|
||||
@ -102,7 +100,7 @@ const Onboarding = ({
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
badToken={badToken}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Grid, Typography, useTheme } from '@mui/material';
|
||||
import { Button, Grid, Typography } from '@mui/material';
|
||||
import { Robot } from '../../models';
|
||||
import TokenInput from './TokenInput';
|
||||
import Key from '@mui/icons-material/Key';
|
||||
@ -10,6 +10,7 @@ interface RecoveryProps {
|
||||
setRobot: (state: Robot) => void;
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
inputToken: string;
|
||||
badToken: string;
|
||||
setInputToken: (state: string) => void;
|
||||
getGenerateRobot: (token: string) => void;
|
||||
}
|
||||
@ -18,21 +19,16 @@ const Recovery = ({
|
||||
robot,
|
||||
setRobot,
|
||||
inputToken,
|
||||
badToken,
|
||||
setView,
|
||||
setInputToken,
|
||||
getGenerateRobot,
|
||||
}: RecoveryProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const recoveryDisabled = () => {
|
||||
return !(inputToken.length > 20);
|
||||
};
|
||||
const onClickRecover = () => {
|
||||
if (recoveryDisabled()) {
|
||||
} else {
|
||||
getGenerateRobot(inputToken);
|
||||
setView('profile');
|
||||
}
|
||||
getGenerateRobot(inputToken);
|
||||
setView('profile');
|
||||
};
|
||||
|
||||
return (
|
||||
@ -56,16 +52,11 @@ const Recovery = ({
|
||||
label={t('Paste token here')}
|
||||
robot={robot}
|
||||
onPressEnter={onClickRecover}
|
||||
badRequest={''}
|
||||
badToken={badToken}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
variant='contained'
|
||||
size='large'
|
||||
disabled={recoveryDisabled()}
|
||||
onClick={onClickRecover}
|
||||
>
|
||||
<Button variant='contained' size='large' disabled={!!badToken} onClick={onClickRecover}>
|
||||
<Key /> <div style={{ width: '0.5em' }} />
|
||||
{t('Recover')}
|
||||
</Button>
|
||||
|
@ -27,12 +27,9 @@ interface RobotProfileProps {
|
||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||
getGenerateRobot: (token: string, slot?: number) => void;
|
||||
inputToken: string;
|
||||
setCurrentOrder: (state: number) => void;
|
||||
logoutRobot: () => void;
|
||||
inputToken: string;
|
||||
setInputToken: (state: string) => void;
|
||||
baseUrl: string;
|
||||
badRequest: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
@ -42,10 +39,8 @@ const RobotProfile = ({
|
||||
inputToken,
|
||||
getGenerateRobot,
|
||||
setInputToken,
|
||||
setCurrentOrder,
|
||||
logoutRobot,
|
||||
setView,
|
||||
badRequest,
|
||||
baseUrl,
|
||||
width,
|
||||
}: RobotProfileProps): JSX.Element => {
|
||||
@ -227,7 +222,6 @@ const RobotProfile = ({
|
||||
label={t('Store your token safely')}
|
||||
setInputToken={setInputToken}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
robot={robot}
|
||||
onPressEnter={() => null}
|
||||
/>
|
||||
|
@ -14,7 +14,7 @@ interface TokenInputProps {
|
||||
inputToken: string;
|
||||
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
|
||||
onPressEnter: () => void;
|
||||
badRequest: string | undefined;
|
||||
badToken?: string;
|
||||
setInputToken: (state: string) => void;
|
||||
showCopy?: boolean;
|
||||
label?: string;
|
||||
@ -30,7 +30,7 @@ const TokenInput = ({
|
||||
onPressEnter,
|
||||
autoFocusTarget = 'textfield',
|
||||
inputToken,
|
||||
badRequest,
|
||||
badToken = '',
|
||||
loading = false,
|
||||
setInputToken,
|
||||
}: TokenInputProps): JSX.Element => {
|
||||
@ -46,7 +46,7 @@ const TokenInput = ({
|
||||
} else {
|
||||
return (
|
||||
<TextField
|
||||
error={!!badRequest}
|
||||
error={inputToken.length > 20 ? !!badToken : false}
|
||||
disabled={!editable}
|
||||
required={true}
|
||||
label={label || undefined}
|
||||
@ -55,7 +55,7 @@ const TokenInput = ({
|
||||
fullWidth={fullWidth}
|
||||
sx={{ borderColor: 'primary' }}
|
||||
variant={editable ? 'outlined' : 'filled'}
|
||||
helperText={badRequest}
|
||||
helperText={badToken}
|
||||
size='medium'
|
||||
onChange={(e) => setInputToken(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
|
@ -20,18 +20,19 @@ import Recovery from './Recovery';
|
||||
import { TorIcon } from '../../components/Icons';
|
||||
import { genKey } from '../../pgp';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { validateTokenEntropy } from '../../utils';
|
||||
|
||||
const RobotPage = (): JSX.Element => {
|
||||
const { robot, setRobot, setCurrentOrder, fetchRobot, torStatus, windowSize, baseUrl } =
|
||||
const { robot, setRobot, fetchRobot, torStatus, windowSize, baseUrl } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const refCode = params.refCode;
|
||||
const url_token = params.token;
|
||||
const width = Math.min(windowSize.width * 0.8, 28);
|
||||
const maxHeight = windowSize.height * 0.85 - 3;
|
||||
const theme = useTheme();
|
||||
|
||||
const [badRequest, setBadRequest] = useState<string | undefined>(undefined);
|
||||
const [badToken, setBadToken] = useState<string>('');
|
||||
const [inputToken, setInputToken] = useState<string>('');
|
||||
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
|
||||
robot.token ? 'profile' : 'welcome',
|
||||
@ -41,26 +42,35 @@ const RobotPage = (): JSX.Element => {
|
||||
if (robot.token) {
|
||||
setInputToken(robot.token);
|
||||
}
|
||||
if (robot.nickname == null && robot.token) {
|
||||
const token = url_token ?? robot.token;
|
||||
if (robot.nickname == null && token) {
|
||||
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
|
||||
fetchRobot({ action: 'generate', setBadRequest });
|
||||
getGenerateRobot(token);
|
||||
setView('profile');
|
||||
}
|
||||
}
|
||||
}, [torStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputToken.length < 20) {
|
||||
setBadToken(t('The token is too short'));
|
||||
} else if (!validateTokenEntropy(inputToken).hasEnoughEntropy) {
|
||||
setBadToken(t('Not enough entropy, make it more complex'));
|
||||
} else {
|
||||
setBadToken('');
|
||||
}
|
||||
}, [inputToken]);
|
||||
|
||||
const getGenerateRobot = (token: string, slot?: number) => {
|
||||
setInputToken(token);
|
||||
genKey(token).then(function (key) {
|
||||
fetchRobot({
|
||||
action: 'generate',
|
||||
newKeys: {
|
||||
pubKey: key.publicKeyArmored,
|
||||
encPrivKey: key.encryptedPrivateKeyArmored,
|
||||
},
|
||||
newToken: token,
|
||||
slot,
|
||||
refCode,
|
||||
setBadRequest,
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -138,7 +148,7 @@ const RobotPage = (): JSX.Element => {
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
@ -151,9 +161,6 @@ const RobotPage = (): JSX.Element => {
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
setCurrentOrder={setCurrentOrder}
|
||||
badRequest={badRequest}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
logoutRobot={logoutRobot}
|
||||
width={width}
|
||||
inputToken={inputToken}
|
||||
@ -168,11 +175,10 @@ const RobotPage = (): JSX.Element => {
|
||||
setView={setView}
|
||||
robot={robot}
|
||||
setRobot={setRobot}
|
||||
badRequest={badRequest}
|
||||
badToken={badToken}
|
||||
inputToken={inputToken}
|
||||
setInputToken={setInputToken}
|
||||
getGenerateRobot={getGenerateRobot}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
) : null}
|
||||
</Paper>
|
||||
|
@ -73,10 +73,6 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
setWeblnEnabled(webln !== undefined);
|
||||
}, []);
|
||||
|
||||
const copyReferralCodeHandler = () => {
|
||||
systemClient.copyToClipboard(`http://${host}/robot/${robot.referralCode}`);
|
||||
};
|
||||
|
||||
const handleWeblnInvoiceClicked = async (e: any) => {
|
||||
e.preventDefault();
|
||||
if (robot.earnedRewards) {
|
||||
@ -94,9 +90,14 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
setShowRewardsSpinner(true);
|
||||
|
||||
apiClient
|
||||
.post(baseUrl, '/api/reward/', {
|
||||
invoice: rewardInvoice,
|
||||
})
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/reward/',
|
||||
{
|
||||
invoice: rewardInvoice,
|
||||
},
|
||||
robot.tokenSHA256,
|
||||
)
|
||||
.then((data: any) => {
|
||||
setBadInvoice(data.bad_invoice ?? '');
|
||||
setShowRewardsSpinner(false);
|
||||
@ -109,7 +110,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
|
||||
const setStealthInvoice = (wantsStealth: boolean) => {
|
||||
apiClient
|
||||
.put(baseUrl, '/api/stealth/', { wantsStealth })
|
||||
.put(baseUrl, '/api/stealth/', { wantsStealth }, robot.tokenSHA256)
|
||||
.then((data) => setRobot({ ...robot, stealthInvoices: data?.wantsStealth }));
|
||||
};
|
||||
|
||||
@ -268,29 +269,6 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PersonAddAltIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText secondary={t('Share to earn 100 Sats per trade')}>
|
||||
<TextField
|
||||
label={t('Your referral link')}
|
||||
value={host + '/robot/' + robot.referralCode}
|
||||
size='small'
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!') || ''}>
|
||||
<IconButton onClick={copyReferralCodeHandler}>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<EmojiEventsIcon />
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Paper } from '@mui/material';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
@ -28,13 +27,13 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
|
||||
this.setState({ hasError: true, error, errorInfo });
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
}, 30000);
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ overflow: 'auto', height: '100%', width: '100%', background: 'white' }}>
|
||||
<h2>Something is borked! Restarting app in 10 seconds...</h2>
|
||||
<h2>Something is borked! Restarting app in 30 seconds...</h2>
|
||||
<p>
|
||||
<b>Error:</b> {this.state.error.name}
|
||||
</p>
|
||||
|
@ -50,7 +50,6 @@ interface MakerFormProps {
|
||||
onReset?: () => void;
|
||||
submitButtonLabel?: string;
|
||||
onOrderCreated?: (id: number) => void;
|
||||
hasRobot?: boolean;
|
||||
}
|
||||
|
||||
const MakerForm = ({
|
||||
@ -61,9 +60,8 @@ const MakerForm = ({
|
||||
onReset = () => {},
|
||||
submitButtonLabel = 'Create Order',
|
||||
onOrderCreated = () => null,
|
||||
hasRobot = true,
|
||||
}: MakerFormProps): JSX.Element => {
|
||||
const { fav, setFav, limits, fetchLimits, info, maker, setMaker, baseUrl } =
|
||||
const { fav, setFav, limits, fetchLimits, info, maker, setMaker, baseUrl, robot } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
const { t } = useTranslation();
|
||||
@ -251,7 +249,7 @@ const MakerForm = ({
|
||||
escrow_duration: maker.escrowDuration,
|
||||
bond_size: maker.bondSize,
|
||||
};
|
||||
apiClient.post(baseUrl, '/api/make/', body).then((data: object) => {
|
||||
apiClient.post(baseUrl, '/api/make/', body, robot.tokenSHA256).then((data: object) => {
|
||||
setBadRequest(data.bad_request);
|
||||
if (data.id) {
|
||||
onOrderCreated(data.id);
|
||||
@ -466,7 +464,7 @@ const MakerForm = ({
|
||||
open={openDialogs}
|
||||
onClose={() => setOpenDialogs(false)}
|
||||
onClickDone={handleCreateOrder}
|
||||
hasRobot={hasRobot}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
/>
|
||||
<Collapse in={limits.list.length == 0}>
|
||||
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
@ -21,16 +21,16 @@ import Countdown from 'react-countdown';
|
||||
import currencies from '../../../static/assets/currencies.json';
|
||||
import { apiClient } from '../../services/api';
|
||||
|
||||
import { Order } from '../../models';
|
||||
import { Order, Info } from '../../models';
|
||||
import { ConfirmationDialog } from '../Dialogs';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { computeSats, pn } from '../../utils';
|
||||
import { computeSats } from '../../utils';
|
||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||
|
||||
interface TakeButtonProps {
|
||||
order: Order;
|
||||
setOrder: (state: Order) => void;
|
||||
baseUrl: string;
|
||||
hasRobot: boolean;
|
||||
info: Info;
|
||||
}
|
||||
|
||||
@ -40,9 +40,10 @@ interface OpenDialogsProps {
|
||||
}
|
||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
||||
|
||||
const TakeButton = ({ order, setOrder, baseUrl, hasRobot, info }: TakeButtonProps): JSX.Element => {
|
||||
const TakeButton = ({ order, setOrder, baseUrl, info }: TakeButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { robot } = useContext<UseAppStoreType>(AppContext);
|
||||
|
||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||
const [badRequest, setBadRequest] = useState<string>('');
|
||||
@ -277,10 +278,15 @@ const TakeButton = ({ order, setOrder, baseUrl, hasRobot, info }: TakeButtonProp
|
||||
const takeOrder = function () {
|
||||
setLoadingTake(true);
|
||||
apiClient
|
||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
||||
action: 'take',
|
||||
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
})
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/order/?order_id=' + order.id,
|
||||
{
|
||||
action: 'take',
|
||||
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
},
|
||||
robot.tokenSHA256,
|
||||
)
|
||||
.then((data) => {
|
||||
setLoadingTake(false);
|
||||
if (data.bad_request) {
|
||||
@ -313,7 +319,7 @@ const TakeButton = ({ order, setOrder, baseUrl, hasRobot, info }: TakeButtonProp
|
||||
setLoadingTake(true);
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
hasRobot={hasRobot}
|
||||
hasRobot={robot.avatarLoaded}
|
||||
/>
|
||||
<InactiveMakerDialog />
|
||||
</Box>
|
||||
|
@ -14,7 +14,7 @@ interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const StringAsIcons: React.FC = ({ othersText, verbose, size, text }: Props) => {
|
||||
const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const parsedText = useMemo(() => {
|
||||
|
@ -14,6 +14,7 @@ import MessageCard from '../MessageCard';
|
||||
import ChatHeader from '../ChatHeader';
|
||||
import { EncryptedChatMessage, ServerMessage } from '..';
|
||||
import ChatBottom from '../ChatBottom';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
interface Props {
|
||||
orderId: number;
|
||||
@ -92,19 +93,24 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
}, [serverMessages]);
|
||||
|
||||
const connectWebsocket = () => {
|
||||
websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => {
|
||||
setConnection(connection);
|
||||
setConnected(true);
|
||||
websocketClient
|
||||
.open(
|
||||
`ws://${window.location.host}/ws/chat/${orderId}/?token_sha256_hex=${sha256(robot.token)}`,
|
||||
)
|
||||
.then((connection) => {
|
||||
setConnection(connection);
|
||||
setConnected(true);
|
||||
|
||||
connection.send({
|
||||
message: robot.pubKey,
|
||||
nick: userNick,
|
||||
connection.send({
|
||||
message: robot.pubKey,
|
||||
nick: userNick,
|
||||
authorization: `Token ${robot.tokenSHA256}`,
|
||||
});
|
||||
|
||||
connection.onMessage((message) => setServerMessages((prev) => [...prev, message]));
|
||||
connection.onClose(() => setConnected(false));
|
||||
connection.onError(() => setConnected(false));
|
||||
});
|
||||
|
||||
connection.onMessage((message) => setServerMessages((prev) => [...prev, message]));
|
||||
connection.onClose(() => setConnected(false));
|
||||
connection.onError(() => setConnected(false));
|
||||
});
|
||||
};
|
||||
|
||||
const createJsonFile: () => object = () => {
|
||||
@ -135,6 +141,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
connection.send({
|
||||
message: `-----SERVE HISTORY-----`,
|
||||
nick: userNick,
|
||||
authorization: `Token ${robot.tokenSHA256}`,
|
||||
});
|
||||
}
|
||||
// If we receive an encrypted message
|
||||
@ -206,6 +213,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
connection.send({
|
||||
message: value,
|
||||
nick: userNick,
|
||||
authorization: `Token ${robot.tokenSHA256}`,
|
||||
});
|
||||
setValue('');
|
||||
}
|
||||
@ -221,6 +229,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
||||
connection.send({
|
||||
message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
nick: userNick,
|
||||
authorization: `Token ${robot.tokenSHA256}`,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
@ -76,7 +76,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
|
||||
const loadMessages: () => void = () => {
|
||||
apiClient
|
||||
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`)
|
||||
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, robot.tokenSHA256)
|
||||
.then((results: any) => {
|
||||
if (results) {
|
||||
setPeerConnected(results.peer_connected);
|
||||
@ -167,11 +167,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
// If input string contains '#' send unencrypted and unlogged message
|
||||
else if (value.substring(0, 1) == '#') {
|
||||
apiClient
|
||||
.post(baseUrl, `/api/chat/`, {
|
||||
PGP_message: value,
|
||||
order_id: orderId,
|
||||
offset: lastIndex,
|
||||
})
|
||||
.post(
|
||||
baseUrl,
|
||||
`/api/chat/`,
|
||||
{
|
||||
PGP_message: value,
|
||||
order_id: orderId,
|
||||
offset: lastIndex,
|
||||
},
|
||||
robot.tokenSHA256,
|
||||
)
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
if (response.messages) {
|
||||
@ -192,11 +197,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
||||
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token)
|
||||
.then((encryptedMessage) => {
|
||||
apiClient
|
||||
.post(baseUrl, `/api/chat/`, {
|
||||
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
order_id: orderId,
|
||||
offset: lastIndex,
|
||||
})
|
||||
.post(
|
||||
baseUrl,
|
||||
`/api/chat/`,
|
||||
{
|
||||
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
||||
order_id: orderId,
|
||||
offset: lastIndex,
|
||||
},
|
||||
robot.tokenSHA256,
|
||||
)
|
||||
.then((response) => {
|
||||
if (response != null) {
|
||||
setPeerConnected(response.peer_connected);
|
||||
|
@ -161,15 +161,20 @@ const TradeBox = ({
|
||||
rating,
|
||||
}: SubmitActionProps) {
|
||||
apiClient
|
||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
||||
action,
|
||||
invoice,
|
||||
routing_budget_ppm,
|
||||
address,
|
||||
mining_fee_rate,
|
||||
statement,
|
||||
rating,
|
||||
})
|
||||
.post(
|
||||
baseUrl,
|
||||
'/api/order/?order_id=' + order.id,
|
||||
{
|
||||
action,
|
||||
invoice,
|
||||
routing_budget_ppm,
|
||||
address,
|
||||
mining_fee_rate,
|
||||
statement,
|
||||
rating,
|
||||
},
|
||||
robot.tokenSHA256,
|
||||
)
|
||||
.catch(() => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
|
@ -18,13 +18,13 @@ import {
|
||||
} from '../models';
|
||||
|
||||
import { apiClient } from '../services/api';
|
||||
import { systemClient } from '../services/System';
|
||||
import { checkVer, getHost, tokenStrength } from '../utils';
|
||||
import { checkVer, getHost, hexToBase91, validateTokenEntropy } from '../utils';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
import defaultCoordinators from '../../static/federation.json';
|
||||
import { createTheme, Theme } from '@mui/material/styles';
|
||||
import i18n from '../i18n/Web';
|
||||
import { systemClient } from '../services/System';
|
||||
|
||||
const getWindowSize = function (fontSize: number) {
|
||||
// returns window size in EM units
|
||||
@ -63,12 +63,10 @@ export interface SlideDirection {
|
||||
}
|
||||
|
||||
export interface fetchRobotProps {
|
||||
action?: 'login' | 'generate' | 'refresh';
|
||||
newKeys?: { encPrivKey: string; pubKey: string } | null;
|
||||
newToken?: string | null;
|
||||
refCode?: string | null;
|
||||
slot?: number | null;
|
||||
setBadRequest?: (state: string) => void;
|
||||
newKeys?: { encPrivKey: string; pubKey: string };
|
||||
newToken?: string;
|
||||
slot?: number;
|
||||
isRefresh?: boolean;
|
||||
}
|
||||
|
||||
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
||||
@ -297,7 +295,9 @@ export const useAppStore = () => {
|
||||
|
||||
const fetchOrder = function () {
|
||||
if (currentOrder != undefined) {
|
||||
apiClient.get(baseUrl, '/api/order/?order_id=' + currentOrder).then(orderReceived);
|
||||
apiClient
|
||||
.get(baseUrl, '/api/order/?order_id=' + currentOrder, robot.tokenSHA256)
|
||||
.then(orderReceived);
|
||||
}
|
||||
};
|
||||
|
||||
@ -307,97 +307,87 @@ export const useAppStore = () => {
|
||||
};
|
||||
|
||||
const fetchRobot = function ({
|
||||
action = 'login',
|
||||
newKeys = null,
|
||||
newToken = null,
|
||||
refCode = null,
|
||||
slot = null,
|
||||
setBadRequest = () => {},
|
||||
}: fetchRobotProps) {
|
||||
const oldRobot = robot;
|
||||
newToken,
|
||||
newKeys,
|
||||
slot,
|
||||
isRefresh = false,
|
||||
}: fetchRobotProps): void {
|
||||
const token = newToken ?? robot.token ?? '';
|
||||
|
||||
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
|
||||
|
||||
if (!hasEnoughEntropy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenSHA256 = hexToBase91(sha256(token));
|
||||
const targetSlot = slot ?? currentSlot;
|
||||
const token = newToken ?? oldRobot.token;
|
||||
if (action != 'refresh') {
|
||||
setRobot(new Robot());
|
||||
}
|
||||
setBadRequest('');
|
||||
const encPrivKey = newKeys?.encPrivKey ?? robot.encPrivKey ?? '';
|
||||
const pubKey = newKeys?.pubKey ?? robot.pubKey ?? '';
|
||||
|
||||
const requestBody = {};
|
||||
if (action == 'login' || action == 'refresh') {
|
||||
requestBody.token_sha256 = sha256(token);
|
||||
} else if (action == 'generate' && token != null) {
|
||||
const strength = tokenStrength(token);
|
||||
requestBody.token_sha256 = sha256(token);
|
||||
requestBody.unique_values = strength.uniqueValues;
|
||||
requestBody.counts = strength.counts;
|
||||
requestBody.length = token.length;
|
||||
requestBody.ref_code = refCode;
|
||||
requestBody.public_key = newKeys?.pubKey ?? oldRobot.pubKey;
|
||||
requestBody.encrypted_private_key = newKeys?.encPrivKey ?? oldRobot.encPrivKey;
|
||||
}
|
||||
// On first authenticated request, pubkey and privkey must be in header cookies
|
||||
systemClient.setCookie('public_key', pubKey.split('\n').join('\\'));
|
||||
systemClient.setCookie('encrypted_private_key', encPrivKey.split('\n').join('\\'));
|
||||
|
||||
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
|
||||
let newRobot = robot;
|
||||
if (currentOrder === undefined) {
|
||||
setCurrentOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: null,
|
||||
);
|
||||
}
|
||||
if (data.bad_request) {
|
||||
setBadRequest(data.bad_request);
|
||||
newRobot = {
|
||||
...oldRobot,
|
||||
loading: false,
|
||||
nickname: data.nickname ?? oldRobot.nickname,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
referralCode: data.referral_code ?? oldRobot.referralCode,
|
||||
earnedRewards: data.earned_rewards ?? oldRobot.earnedRewards,
|
||||
lastOrderId: data.last_order_id ?? oldRobot.lastOrderId,
|
||||
stealthInvoices: data.wants_stealth ?? robot.stealthInvoices,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
found: false,
|
||||
if (!isRefresh) {
|
||||
setRobot((robot) => {
|
||||
return {
|
||||
...robot,
|
||||
loading: true,
|
||||
avatarLoaded: false,
|
||||
};
|
||||
} else {
|
||||
newRobot = {
|
||||
...oldRobot,
|
||||
});
|
||||
}
|
||||
|
||||
apiClient
|
||||
.get(baseUrl, '/api/robot/', tokenSHA256)
|
||||
.then((data: any) => {
|
||||
const newRobot = {
|
||||
avatarLoaded: isRefresh ? robot.avatarLoaded : false,
|
||||
nickname: data.nickname,
|
||||
token,
|
||||
tokenSHA256,
|
||||
loading: false,
|
||||
activeOrderId: data.active_order_id ?? null,
|
||||
lastOrderId: data.last_order_id ?? null,
|
||||
referralCode: data.referral_code,
|
||||
earnedRewards: data.earned_rewards ?? 0,
|
||||
stealthInvoices: data.wants_stealth,
|
||||
tgEnabled: data.tg_enabled,
|
||||
tgBotName: data.tg_bot_name,
|
||||
tgToken: data.tg_token,
|
||||
found: data?.found,
|
||||
bitsEntropy: data.token_bits_entropy,
|
||||
shannonEntropy: data.token_shannon_entropy,
|
||||
last_login: data.last_login,
|
||||
bitsEntropy,
|
||||
shannonEntropy,
|
||||
pubKey: data.public_key,
|
||||
encPrivKey: data.encrypted_private_key,
|
||||
copiedToken: !!data.found,
|
||||
};
|
||||
if (currentOrder === undefined) {
|
||||
setCurrentOrder(
|
||||
data.active_order_id
|
||||
? data.active_order_id
|
||||
: data.last_order_id
|
||||
? data.last_order_id
|
||||
: null,
|
||||
);
|
||||
}
|
||||
setRobot(newRobot);
|
||||
garage.updateRobot(newRobot, targetSlot);
|
||||
setCurrentSlot(targetSlot);
|
||||
systemClient.setItem('robot_token', token);
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
systemClient.deleteCookie('public_key');
|
||||
systemClient.deleteCookie('encrypted_private_key');
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (baseUrl != '' && page != 'robot') {
|
||||
if (open.profile && robot.avatarLoaded) {
|
||||
fetchRobot({ action: 'refresh' }); // refresh/update existing robot
|
||||
fetchRobot({ isRefresh: true }); // refresh/update existing robot
|
||||
} else if (!robot.avatarLoaded && robot.token && robot.encPrivKey && robot.pubKey) {
|
||||
fetchRobot({ action: 'generate' }); // create new robot with existing token and keys (on network and coordinator change)
|
||||
fetchRobot({}); // create new robot with existing token and keys (on network and coordinator change)
|
||||
}
|
||||
}
|
||||
}, [open.profile, baseUrl]);
|
||||
|
@ -2,6 +2,7 @@ class Robot {
|
||||
constructor(garageRobot?: Robot) {
|
||||
if (garageRobot != null) {
|
||||
this.token = garageRobot?.token ?? undefined;
|
||||
this.tokenSHA256 = garageRobot?.tokenSHA256 ?? undefined;
|
||||
this.pubKey = garageRobot?.pubKey ?? undefined;
|
||||
this.encPrivKey = garageRobot?.encPrivKey ?? undefined;
|
||||
}
|
||||
@ -9,20 +10,21 @@ class Robot {
|
||||
|
||||
public nickname?: string;
|
||||
public token?: string;
|
||||
public pubKey?: string;
|
||||
public encPrivKey?: string;
|
||||
public bitsEntropy?: number;
|
||||
public shannonEntropy?: number;
|
||||
public tokenSHA256?: string;
|
||||
public pubKey?: string;
|
||||
public encPrivKey?: string;
|
||||
public stealthInvoices: boolean = true;
|
||||
public activeOrderId?: number;
|
||||
public lastOrderId?: number;
|
||||
public earnedRewards: number = 0;
|
||||
public referralCode: string = '';
|
||||
public tgEnabled: boolean = false;
|
||||
public tgBotName: string = 'unknown';
|
||||
public tgToken: string = 'unknown';
|
||||
public loading: boolean = false;
|
||||
public found: boolean = false;
|
||||
public last_login: string = '';
|
||||
public avatarLoaded: boolean = false;
|
||||
public copiedToken: boolean = false;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class SystemWebClient implements SystemClient {
|
||||
};
|
||||
|
||||
public deleteCookie: (key: string) => void = (key) => {
|
||||
document.cookie = `${name}= ; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
document.cookie = `${key}= ;path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT`;
|
||||
};
|
||||
|
||||
// Local storage
|
||||
|
@ -5,21 +5,27 @@ class ApiNativeClient implements ApiClient {
|
||||
private assetsCache: { [path: string]: string } = {};
|
||||
private assetsPromises: { [path: string]: Promise<string | undefined> } = {};
|
||||
|
||||
private readonly getHeaders: () => HeadersInit = () => {
|
||||
private readonly getHeaders: (tokenSHA256?: string) => HeadersInit = (tokenSHA256) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const robotToken = systemClient.getItem('robot_token');
|
||||
if (robotToken) {
|
||||
const sessionid = systemClient.getCookie('sessionid');
|
||||
// const csrftoken = systemClient.getCookie('csrftoken');
|
||||
|
||||
if (tokenSHA256) {
|
||||
headers = {
|
||||
...headers,
|
||||
...{
|
||||
// 'X-CSRFToken': csrftoken,
|
||||
Cookie: `sessionid=${sessionid}`, // ;csrftoken=${csrftoken}
|
||||
Authorization: `Token ${tokenSHA256.substring(0, 40)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const encrypted_private_key = systemClient.getCookie('encrypted_private_key');
|
||||
const public_key = systemClient.getCookie('public_key');
|
||||
|
||||
if (encrypted_private_key && public_key) {
|
||||
headers = {
|
||||
...headers,
|
||||
...{
|
||||
Cookie: `public_key=${public_key};encrypted_private_key=${encrypted_private_key}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -45,44 +51,47 @@ class ApiNativeClient implements ApiClient {
|
||||
return await new Promise((res, _rej) => res({}));
|
||||
};
|
||||
|
||||
public delete: (baseUrl: string, path: string) => Promise<object | undefined> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
) => {
|
||||
public delete: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object | undefined> = async (baseUrl, path, tokenSHA256) => {
|
||||
return await window.NativeRobosats?.postMessage({
|
||||
category: 'http',
|
||||
type: 'delete',
|
||||
baseUrl,
|
||||
path,
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
}).then(this.parseResponse);
|
||||
};
|
||||
|
||||
public post: (baseUrl: string, path: string, body: object) => Promise<object | undefined> =
|
||||
async (baseUrl, path, body) => {
|
||||
return await window.NativeRobosats?.postMessage({
|
||||
category: 'http',
|
||||
type: 'post',
|
||||
baseUrl,
|
||||
path,
|
||||
body,
|
||||
headers: this.getHeaders(),
|
||||
}).then(this.parseResponse);
|
||||
};
|
||||
|
||||
public get: (baseUrl: string, path: string) => Promise<object | undefined> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
) => {
|
||||
public post: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: object,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object | undefined> = async (baseUrl, path, body, tokenSHA256) => {
|
||||
return await window.NativeRobosats?.postMessage({
|
||||
category: 'http',
|
||||
type: 'get',
|
||||
type: 'post',
|
||||
baseUrl,
|
||||
path,
|
||||
headers: this.getHeaders(),
|
||||
body,
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
}).then(this.parseResponse);
|
||||
};
|
||||
|
||||
public get: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object | undefined> =
|
||||
async (baseUrl, path, tokenSHA256) => {
|
||||
return await window.NativeRobosats?.postMessage({
|
||||
category: 'http',
|
||||
type: 'get',
|
||||
baseUrl,
|
||||
path,
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
}).then(this.parseResponse);
|
||||
};
|
||||
|
||||
public fileImageUrl: (baseUrl: string, path: string) => Promise<string | undefined> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
|
@ -1,22 +1,32 @@
|
||||
import { ApiClient } from '..';
|
||||
import { systemClient } from '../../System';
|
||||
|
||||
class ApiWebClient implements ApiClient {
|
||||
private readonly getHeaders: () => HeadersInit = () => {
|
||||
return {
|
||||
private readonly getHeaders: (tokenSHA256?: string) => HeadersInit = (tokenSHA256) => {
|
||||
let headers = {
|
||||
'Content-Type': 'application/json',
|
||||
// 'X-CSRFToken': systemClient.getCookie('csrftoken') || '',
|
||||
};
|
||||
|
||||
if (tokenSHA256) {
|
||||
headers = {
|
||||
...headers,
|
||||
...{
|
||||
Authorization: `Token ${tokenSHA256.substring(0, 40)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
public post: (baseUrl: string, path: string, body: object) => Promise<object> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
body,
|
||||
) => {
|
||||
public post: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: object,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object> = async (baseUrl, path, body, tokenSHA256) => {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
|
||||
@ -25,14 +35,15 @@ class ApiWebClient implements ApiClient {
|
||||
);
|
||||
};
|
||||
|
||||
public put: (baseUrl: string, path: string, body: object) => Promise<object> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
body,
|
||||
) => {
|
||||
public put: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: object,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object> = async (baseUrl, path, body, tokenSHA256) => {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return await fetch(baseUrl + path, requestOptions).then(
|
||||
@ -40,18 +51,28 @@ class ApiWebClient implements ApiClient {
|
||||
);
|
||||
};
|
||||
|
||||
public delete: (baseUrl: string, path: string) => Promise<object> = async (baseUrl, path) => {
|
||||
public delete: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
tokenSHA256,
|
||||
) => {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(),
|
||||
headers: this.getHeaders(tokenSHA256),
|
||||
};
|
||||
return await fetch(baseUrl + path, requestOptions).then(
|
||||
async (response) => await response.json(),
|
||||
);
|
||||
};
|
||||
|
||||
public get: (baseUrl: string, path: string) => Promise<object> = async (baseUrl, path) => {
|
||||
return await fetch(baseUrl + path).then(async (response) => await response.json());
|
||||
public get: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object> = async (
|
||||
baseUrl,
|
||||
path,
|
||||
tokenSHA256,
|
||||
) => {
|
||||
return await fetch(baseUrl + path, { headers: this.getHeaders(tokenSHA256) }).then(
|
||||
async (response) => await response.json(),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,20 @@ import ApiWebClient from './ApiWebClient';
|
||||
import ApiNativeClient from './ApiNativeClient';
|
||||
|
||||
export interface ApiClient {
|
||||
post: (baseUrl: string, path: string, body: object) => Promise<object | undefined>;
|
||||
put: (baseUrl: string, path: string, body: object) => Promise<object | undefined>;
|
||||
get: (baseUrl: string, path: string) => Promise<object | undefined>;
|
||||
delete: (baseUrl: string, path: string) => Promise<object | undefined>;
|
||||
post: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: object,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object | undefined>;
|
||||
put: (
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
body: object,
|
||||
tokenSHA256?: string,
|
||||
) => Promise<object | undefined>;
|
||||
get: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object | undefined>;
|
||||
delete: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object | undefined>;
|
||||
fileImageUrl?: (baseUrl: string, path: string) => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
|
10
frontend/src/utils/hexToBase91.ts
Normal file
10
frontend/src/utils/hexToBase91.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Base91 } from 'base-ex';
|
||||
|
||||
export default function hexToBase85(hex: string): string {
|
||||
const hexes = hex.match(/.{1,2}/g);
|
||||
if (!hexes) return '';
|
||||
const byteArray = hexes.map((byte) => parseInt(byte, 16));
|
||||
const b91 = new Base91();
|
||||
const base91string = b91.encode(new Uint8Array(byteArray));
|
||||
return base91string;
|
||||
}
|
@ -2,11 +2,12 @@ export { default as checkVer } from './checkVer';
|
||||
export { default as filterOrders } from './filterOrders';
|
||||
export { default as getHost } from './getHost';
|
||||
export { default as hexToRgb } from './hexToRgb';
|
||||
export { default as hexToBase91 } from './hexToBase91';
|
||||
export { default as matchMedian } from './match';
|
||||
export { default as pn } from './prettyNumbers';
|
||||
export { amountToString } from './prettyNumbers';
|
||||
export { default as saveAsJson } from './saveFile';
|
||||
export { default as statusBadgeColor } from './statusBadgeColor';
|
||||
export { genBase62Token, tokenStrength } from './token';
|
||||
export { genBase62Token, validateTokenEntropy } from './token';
|
||||
export { default as getWebln } from './webln';
|
||||
export { default as computeSats } from './computeSats';
|
||||
|
@ -1,19 +0,0 @@
|
||||
// sort of cryptographically strong function to generate Base62 token client-side
|
||||
export function genBase62Token(length) {
|
||||
return window
|
||||
.btoa(
|
||||
Array.from(window.crypto.getRandomValues(new Uint8Array(length * 2)))
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join(''),
|
||||
)
|
||||
.replace(/[+/]/g, '')
|
||||
.substring(0, length);
|
||||
}
|
||||
|
||||
export function tokenStrength(token) {
|
||||
const characters = token.split('').reduce(function (obj, s) {
|
||||
obj[s] = (obj[s] || 0) + 1;
|
||||
return obj;
|
||||
}, {});
|
||||
return { uniqueValues: Object.keys(characters).length, counts: Object.values(characters) };
|
||||
}
|
45
frontend/src/utils/token.ts
Normal file
45
frontend/src/utils/token.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// sort of cryptographically strong function to generate Base62 token client-side
|
||||
export function genBase62Token(length: number): string {
|
||||
return window
|
||||
.btoa(
|
||||
Array.from(window.crypto.getRandomValues(new Uint8Array(length * 2)))
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join(''),
|
||||
)
|
||||
.replace(/[+/]/g, '')
|
||||
.substring(0, length);
|
||||
}
|
||||
|
||||
interface TokenEntropy {
|
||||
hasEnoughEntropy: boolean;
|
||||
bitsEntropy: number;
|
||||
shannonEntropy: number;
|
||||
}
|
||||
|
||||
export function validateTokenEntropy(token: string): TokenEntropy {
|
||||
const charCounts: Record<string, number> = {};
|
||||
const len = token.length;
|
||||
let shannonEntropy = 0;
|
||||
|
||||
// Count number of occurrences of each character
|
||||
for (let i = 0; i < len; i++) {
|
||||
const char = token.charAt(i);
|
||||
if (charCounts[char]) {
|
||||
charCounts[char]++;
|
||||
} else {
|
||||
charCounts[char] = 1;
|
||||
}
|
||||
}
|
||||
// Calculate the entropy
|
||||
Object.keys(charCounts).forEach((char) => {
|
||||
const probability = charCounts[char] / len;
|
||||
shannonEntropy -= probability * Math.log2(probability);
|
||||
});
|
||||
|
||||
const uniqueChars = Object.keys(charCounts).length;
|
||||
const bitsEntropy = Math.log2(Math.pow(uniqueChars, len));
|
||||
|
||||
const hasEnoughEntropy = bitsEntropy > 128 && shannonEntropy > 4;
|
||||
|
||||
return { hasEnoughEntropy, bitsEntropy, shannonEntropy };
|
||||
}
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Aprèn RoboSats",
|
||||
"See profile": "Veure perfil",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connectant a TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connexió xifrada i anònima mitjançant TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "Això garanteix la màxima privadesa, però és possible que sentis que l'aplicació es comporta lenta. Si es perd la connexió, reinicia l'aplicació.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram activat",
|
||||
"Enable Telegram Notifications": "Notificar en Telegram",
|
||||
"Use stealth invoices": "Factures ofuscades",
|
||||
"Share to earn 100 Sats per trade": "Comparteix per a guanyar 100 Sats por intercanvi",
|
||||
"Your referral link": "El teu enllaç de referits",
|
||||
"Your earned rewards": "Les teves recompenses guanyades",
|
||||
"Claim": "Retirar",
|
||||
"Invoice for {{amountSats}} Sats": "Factura per {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Více o RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram povolen",
|
||||
"Enable Telegram Notifications": "Povolit Telegram notifikace",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Sdílej a získej za každý obchod 100 Satů",
|
||||
"Your referral link": "Tvůj referral odkaz",
|
||||
"Your earned rewards": "Tvé odměny",
|
||||
"Claim": "Vybrat",
|
||||
"Invoice for {{amountSats}} Sats": "Invoice pro {{amountSats}} Satů",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Lerne RoboSats kennen",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram aktiviert",
|
||||
"Enable Telegram Notifications": "Telegram-Benachrichtigungen aktivieren",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Teilen, um 100 Sats pro Handel zu verdienen",
|
||||
"Your referral link": "Dein Empfehlungslink",
|
||||
"Your earned rewards": "Deine verdienten Belohnungen",
|
||||
"Claim": "Erhalten",
|
||||
"Invoice for {{amountSats}} Sats": "Invoice für {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Learn RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram enabled",
|
||||
"Enable Telegram Notifications": "Enable Telegram Notifications",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Share to earn 100 Sats per trade",
|
||||
"Your referral link": "Your referral link",
|
||||
"Your earned rewards": "Your earned rewards",
|
||||
"Claim": "Claim",
|
||||
"Invoice for {{amountSats}} Sats": "Invoice for {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Aprende RoboSats",
|
||||
"See profile": "Ver perfil",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Conectando con TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Conexión encriptada y anonimizada usando TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "Esto asegura máxima privacidad, aunque quizá observe algo de lentitud. Si se corta la conexión, reinicie la aplicación.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram activado",
|
||||
"Enable Telegram Notifications": "Notificar en Telegram",
|
||||
"Use stealth invoices": "Facturas sigilosas",
|
||||
"Share to earn 100 Sats per trade": "Comparte para ganar 100 Sats por intercambio",
|
||||
"Your referral link": "Tu enlace de referido",
|
||||
"Your earned rewards": "Tus recompensas ganadas",
|
||||
"Claim": "Reclamar",
|
||||
"Invoice for {{amountSats}} Sats": "Factura de {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Ikasi RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram baimendua",
|
||||
"Enable Telegram Notifications": "Baimendu Telegram Jakinarazpenak",
|
||||
"Use stealth invoices": "Erabili ezkutuko fakturak",
|
||||
"Share to earn 100 Sats per trade": "Partekatu 100 Sat trukeko irabazteko",
|
||||
"Your referral link": "Zure erreferentziako esteka",
|
||||
"Your earned rewards": "Irabazitako sariak",
|
||||
"Claim": "Eskatu",
|
||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} Sateko fakura",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Learn RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram activé",
|
||||
"Enable Telegram Notifications": "Activer les notifications Telegram",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Partagez pour gagner 100 Sats par transaction",
|
||||
"Your referral link": "Votre lien de parrainage",
|
||||
"Your earned rewards": "Vos récompenses gagnées",
|
||||
"Claim": "Réclamer",
|
||||
"Invoice for {{amountSats}} Sats": "Facture pour {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Impara RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram attivato",
|
||||
"Enable Telegram Notifications": "Attiva notifiche Telegram",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Condividi e guadagna 100 Sats con ogni transazione",
|
||||
"Your referral link": "Il tuo link di riferimento",
|
||||
"Your earned rewards": "La tua ricompensa",
|
||||
"Claim": "Riscatta",
|
||||
"Invoice for {{amountSats}} Sats": "Ricevuta per {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Robosatsを学ぶ",
|
||||
"See profile": "プロフィール",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Torネットワークに接続中",
|
||||
"Connection encrypted and anonymized using TOR.": "接続はTORを使って暗号化され、匿名化されています。",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "これにより最大限のプライバシーが確保されますが、アプリの動作が遅いと感じることがあります。接続が切断された場合は、アプリを再起動してください。",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegramが有効になりました",
|
||||
"Enable Telegram Notifications": "Telegram通知を有効にする",
|
||||
"Use stealth invoices": "ステルス・インボイスを使用する",
|
||||
"Share to earn 100 Sats per trade": "共有して取引ごとに100 Satsを稼ぐ",
|
||||
"Your referral link": "あなたの紹介リンク",
|
||||
"Your earned rewards": "あなたの獲得報酬",
|
||||
"Claim": "請求する",
|
||||
"Invoice for {{amountSats}} Sats": " {{amountSats}} Satsのインボイス",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Learn RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram włączony",
|
||||
"Enable Telegram Notifications": "Włącz powiadomienia telegramu",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Udostępnij, aby zarobić 100 Sats na transakcję",
|
||||
"Your referral link": "Twój link referencyjny",
|
||||
"Your earned rewards": "Twoje zarobione nagrody",
|
||||
"Claim": "Prawo",
|
||||
"Invoice for {{amountSats}} Sats": "Faktura za {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Aprender sobre o RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram ativado",
|
||||
"Enable Telegram Notifications": "Habilitar notificações do Telegram",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Compartilhe para ganhar 100 Sats por negociação",
|
||||
"Your referral link": "Seu link de referência",
|
||||
"Your earned rewards": "Suas recompensas ganhas",
|
||||
"Claim": "Reinvindicar",
|
||||
"Invoice for {{amountSats}} Sats": "Invoice para {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Изучить RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram включен",
|
||||
"Enable Telegram Notifications": "Включить уведомления Telegram",
|
||||
"Use stealth invoices": "Использовать стелс инвойсы",
|
||||
"Share to earn 100 Sats per trade": "Поделись, чтобы заработать 100 Сатоши за сделку",
|
||||
"Your referral link": "Ваша реферальная ссылка",
|
||||
"Your earned rewards": "Ваши заработанные награды",
|
||||
"Claim": "Запросить",
|
||||
"Invoice for {{amountSats}} Sats": "Инвойс на {{amountSats}} Сатоши",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "Learn RoboSats",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram aktiverat",
|
||||
"Enable Telegram Notifications": "Aktivera Telegram-notiser",
|
||||
"Use stealth invoices": "Use stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Dela för att tjäna 100 sats per trade",
|
||||
"Your referral link": "Din referrallänk",
|
||||
"Your earned rewards": "Du fick belöningar",
|
||||
"Claim": "Claim",
|
||||
"Invoice for {{amountSats}} Sats": "Faktura för {{amountSats}} sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "เรียนรู้การใช้งาน",
|
||||
"See profile": "See profile",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "Connecting to TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using TOR.",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "เปิดใช้ Telegram แล้ว",
|
||||
"Enable Telegram Notifications": "เปิดใช้การแจ้งเตือนผ่านทาง Telegram",
|
||||
"Use stealth invoices": "ใช้งาน stealth invoices",
|
||||
"Share to earn 100 Sats per trade": "Share เพื่อรับ 100 Sats ต่อการซื้อขาย",
|
||||
"Your referral link": "ลิ้งค์ referral ของคุณ",
|
||||
"Your earned rewards": "รางวัลที่ตุณได้รับ",
|
||||
"Claim": "รับรางวัล",
|
||||
"Invoice for {{amountSats}} Sats": "Invoice สำหรับ {{amountSats}} Sats",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "学习 RoboSats",
|
||||
"See profile": "查看个人资料",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "正在连线 TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "连接已用 TOR 加密和匿名化。",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "这确保最高的隐秘程度,但你可能会觉得应用程序运作缓慢。如果丢失连接,请重启应用。",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram 已开启",
|
||||
"Enable Telegram Notifications": "开启 Telegram 通知",
|
||||
"Use stealth invoices": "使用隐形发票",
|
||||
"Share to earn 100 Sats per trade": "分享并于每笔交易获得100聪的奖励",
|
||||
"Your referral link": "你的推荐链接",
|
||||
"Your earned rewards": "你获得的奖励",
|
||||
"Claim": "领取",
|
||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聪的发票",
|
||||
|
@ -40,6 +40,8 @@
|
||||
"Learn RoboSats": "學習 RoboSats",
|
||||
"See profile": "查看個人資料",
|
||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
||||
"The token is too short": "The token is too short",
|
||||
"Not enough entropy, make it more complex": "Not enough entropy, make it more complex",
|
||||
"Connecting to TOR": "正在連線 TOR",
|
||||
"Connection encrypted and anonymized using TOR.": "連接已用 TOR 加密和匿名化。",
|
||||
"This ensures maximum privacy, however you might feel the app behaves slow. If connection is lost, restart the app.": "這確保最高的隱密程度,但你可能會覺得應用程序運作緩慢。如果丟失連接,請重啟應用。",
|
||||
@ -252,8 +254,6 @@
|
||||
"Telegram enabled": "Telegram 已開啟",
|
||||
"Enable Telegram Notifications": "開啟 Telegram 通知",
|
||||
"Use stealth invoices": "使用隱形發票",
|
||||
"Share to earn 100 Sats per trade": "分享並於每筆交易獲得100聰的獎勵",
|
||||
"Your referral link": "你的推薦鏈接",
|
||||
"Your earned rewards": "你獲得的獎勵",
|
||||
"Claim": "領取",
|
||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聰的發票",
|
||||
|
@ -6,7 +6,7 @@ urlpatterns = [
|
||||
path("", basic),
|
||||
path("create/", basic),
|
||||
path("robot/", basic),
|
||||
path("robot/<refCode>", basic),
|
||||
path("robot/<token>", basic),
|
||||
path("offers/", basic),
|
||||
path("order/<int:orderId>", basic),
|
||||
path("settings/", basic),
|
||||
|
@ -33,3 +33,4 @@ isort==5.12.0
|
||||
flake8==6.0.0
|
||||
pyflakes==3.0.1
|
||||
django-cors-headers==3.14.0
|
||||
base91==1.0.1
|
||||
|
@ -1,3 +1,26 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from channels.db import database_sync_to_async
|
||||
from channels.middleware import BaseMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.db import IntegrityError
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from robohash import Robohash
|
||||
|
||||
from api.nick_generator.nick_generator import NickGenerator
|
||||
from api.utils import base91_to_hex, hex_to_base91, is_valid_token, validate_pgp_keys
|
||||
|
||||
NickGen = NickGenerator(
|
||||
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
||||
)
|
||||
|
||||
avatar_path = Path(settings.AVATAR_ROOT)
|
||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class DisableCSRFMiddleware(object):
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
@ -6,3 +29,140 @@ class DisableCSRFMiddleware(object):
|
||||
setattr(request, "_dont_enforce_csrf_checks", True)
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
||||
class RobotTokenSHA256AuthenticationMiddleWare:
|
||||
"""
|
||||
Builds on django-rest-framework Token Authentication.
|
||||
|
||||
The robot token SHA256 is taken from the header. The token SHA256 must
|
||||
be encoded as Base91 of 39 or 40 characters in length. This is the max length of
|
||||
django DRF token keys.
|
||||
|
||||
If the token exists, the requests passes through. If the token is valid and new,
|
||||
a new user/robot is created (PGP keys are required in the request body).
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
token_sha256_b91 = request.META.get("HTTP_AUTHORIZATION", "").replace(
|
||||
"Token ", ""
|
||||
)
|
||||
|
||||
if not token_sha256_b91:
|
||||
# Unauthenticated request
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
if not is_valid_token(token_sha256_b91):
|
||||
raise AuthenticationFailed(
|
||||
"Robot token SHA256 was provided in the header. However it is not a valid 39 or 40 characters Base91 string."
|
||||
)
|
||||
|
||||
# Check if it is an existing robot.
|
||||
try:
|
||||
Token.objects.get(key=token_sha256_b91)
|
||||
|
||||
except Token.DoesNotExist:
|
||||
# If we get here the user does not have a robot on this coordinator
|
||||
# Let's create a new user & robot on-the-fly.
|
||||
|
||||
# The first ever request to a coordinator must include cookies for the public key (and encrypted priv key as of now).
|
||||
public_key = request.COOKIES.get("public_key")
|
||||
encrypted_private_key = request.COOKIES.get("encrypted_private_key", "")
|
||||
|
||||
if not public_key or not encrypted_private_key:
|
||||
raise AuthenticationFailed(
|
||||
"On the first request to a RoboSats coordinator, you must provide as well a valid public and encrypted private PGP keys"
|
||||
)
|
||||
|
||||
(
|
||||
valid,
|
||||
bad_keys_context,
|
||||
public_key,
|
||||
encrypted_private_key,
|
||||
) = validate_pgp_keys(public_key, encrypted_private_key)
|
||||
if not valid:
|
||||
raise AuthenticationFailed(bad_keys_context)
|
||||
|
||||
# Hash the token_sha256, only 1 iteration.
|
||||
# This is the second SHA256 of the user token, aka RoboSats ID
|
||||
token_sha256 = base91_to_hex(token_sha256_b91)
|
||||
hash = hashlib.sha256(token_sha256.encode("utf-8")).hexdigest()
|
||||
|
||||
# Generate nickname deterministically
|
||||
nickname = NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
|
||||
# DEPRECATE. Using Try and Except only as a temporary measure.
|
||||
# This will allow existing robots to be added upgraded with a token.key
|
||||
# After v0.5.0, only the following should remain
|
||||
# `user = User.objects.create_user(username=nickname, password=None)`
|
||||
try:
|
||||
user = User.objects.create_user(username=nickname, password=None)
|
||||
except IntegrityError:
|
||||
# UNIQUE constrain failed, user exist. Get it.
|
||||
user = User.objects.get(username=nickname)
|
||||
|
||||
# Django rest_framework authtokens are limited to 40 characters.
|
||||
# We use base91 so we can store the full entropy in the field.
|
||||
Token.objects.create(key=token_sha256_b91, user=user)
|
||||
|
||||
# Add PGP keys to the new user
|
||||
if not user.robot.public_key:
|
||||
user.robot.public_key = public_key
|
||||
if not user.robot.encrypted_private_key:
|
||||
user.robot.encrypted_private_key = encrypted_private_key
|
||||
|
||||
# Generate avatar. Does not replace if existing.
|
||||
image_path = avatar_path.joinpath(nickname + ".webp")
|
||||
if not image_path.exists():
|
||||
|
||||
rh = Robohash(hash)
|
||||
rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
|
||||
with open(image_path, "wb") as f:
|
||||
rh.img.save(f, format="WEBP", quality=80)
|
||||
|
||||
image_small_path = avatar_path.joinpath(nickname + ".small.webp")
|
||||
with open(image_small_path, "wb") as f:
|
||||
resized_img = rh.img.resize((80, 80))
|
||||
resized_img.save(f, format="WEBP", quality=80)
|
||||
|
||||
user.robot.avatar = "static/assets/avatars/" + nickname + ".webp"
|
||||
user.save()
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
|
||||
# Authenticate WebSockets connections using DRF tokens
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user(token_key):
|
||||
try:
|
||||
token = Token.objects.get(key=token_key)
|
||||
return token.user
|
||||
except Token.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
class TokenAuthMiddleware(BaseMiddleware):
|
||||
def __init__(self, inner):
|
||||
super().__init__(inner)
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
try:
|
||||
token_key = (
|
||||
dict((x.split("=") for x in scope["query_string"].decode().split("&")))
|
||||
).get("token_sha256_hex", None)
|
||||
token_key = hex_to_base91(token_key)
|
||||
print(token_key)
|
||||
except ValueError:
|
||||
token_key = None
|
||||
scope["user"] = (
|
||||
AnonymousUser() if token_key is None else await get_user(token_key)
|
||||
)
|
||||
return await super().__call__(scope, receive, send)
|
||||
|
@ -6,6 +6,7 @@ from decouple import config
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
import chat.routing
|
||||
from robosats.middleware import TokenAuthMiddleware
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
@ -14,9 +15,11 @@ django_asgi_app = get_asgi_application()
|
||||
|
||||
protocols = {}
|
||||
protocols["websocket"] = AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns,
|
||||
# add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||
TokenAuthMiddleware(
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns,
|
||||
# add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -11,6 +11,7 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
@ -34,6 +35,9 @@ DEBUG = False
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = "/usr/src/static/"
|
||||
|
||||
# RoboSats version
|
||||
with open("version.json") as f:
|
||||
VERSION = json.load(f)
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
if config("DEVELOPMENT", default=False):
|
||||
@ -92,6 +96,7 @@ INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
"corsheaders",
|
||||
"rest_framework",
|
||||
"rest_framework.authtoken",
|
||||
"django_celery_beat",
|
||||
"django_celery_results",
|
||||
"import_export",
|
||||
@ -105,10 +110,13 @@ INSTALLED_APPS = [
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
],
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "RoboSats REST API v0",
|
||||
"TITLE": "RoboSats REST API",
|
||||
"DESCRIPTION": textwrap.dedent(
|
||||
"""
|
||||
REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange
|
||||
@ -123,7 +131,7 @@ SPECTACULAR_SETTINGS = {
|
||||
|
||||
"""
|
||||
),
|
||||
"VERSION": "0.1.0",
|
||||
"VERSION": f"{VERSION['major']}.{VERSION['minor']}.{VERSION['patch']}",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
@ -145,9 +153,9 @@ MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
# "django.middleware.csrf.CsrfViewMiddleware",
|
||||
"robosats.middleware.DisableCSRFMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"robosats.middleware.RobotTokenSHA256AuthenticationMiddleWare",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
|
@ -13,12 +13,21 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from decouple import config
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
VERSION = settings.VERSION
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("coordinator/", admin.site.urls),
|
||||
path("api/", include("api.urls")),
|
||||
# path('chat/', include('chat.urls')),
|
||||
path("", include("frontend.urls")),
|
||||
]
|
||||
|
||||
admin.site.site_header = f"RoboSats Coordinator: {config('COORDINATOR_ALIAS', cast=str, default='NoAlias')} (v{VERSION['major']}.{VERSION['minor']}.{VERSION['patch']})"
|
||||
admin.site.index_title = "Coordinator administration"
|
||||
admin.site.site_title = "RoboSats Coordinator Admin"
|
||||
|
Loading…
Reference in New Issue
Block a user