mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-05 22:10:10 +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 directory to read TLS cert and macaroon
|
||||||
LND_DIR='/lnd/'
|
LND_DIR='/lnd/'
|
||||||
MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
|
MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
|
||||||
|
@ -2,7 +2,6 @@ import ast
|
|||||||
import math
|
import math
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import gnupg
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q, Sum
|
from django.db.models import Q, Sum
|
||||||
@ -88,56 +87,6 @@ class Logics:
|
|||||||
|
|
||||||
return True, None, None
|
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
|
@classmethod
|
||||||
def validate_order_size(cls, order):
|
def validate_order_size(cls, order):
|
||||||
"""Validates if order size in Sats is within limits at t0"""
|
"""Validates if order size in Sats is within limits at t0"""
|
||||||
|
@ -11014,7 +11014,7 @@ nouns = [
|
|||||||
"Zostera",
|
"Zostera",
|
||||||
"Zosterops",
|
"Zosterops",
|
||||||
"Zouave",
|
"Zouave",
|
||||||
"Zu/is",
|
"Zuis",
|
||||||
"Zubr",
|
"Zubr",
|
||||||
"Zucchini",
|
"Zucchini",
|
||||||
"Zuche",
|
"Zuche",
|
||||||
|
@ -467,6 +467,23 @@ class UserViewSchema:
|
|||||||
"description": "Whether the user prefers stealth invoices",
|
"description": "Whether the user prefers stealth invoices",
|
||||||
},
|
},
|
||||||
"found": {"type": "string", "description": "Welcome back message"},
|
"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": {
|
"active_order_id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Active order id if present",
|
"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:
|
class InfoViewSchema:
|
||||||
get = {
|
get = {
|
||||||
"summary": "Get info",
|
"summary": "Get info",
|
||||||
|
@ -12,6 +12,7 @@ from .views import (
|
|||||||
OrderView,
|
OrderView,
|
||||||
PriceView,
|
PriceView,
|
||||||
RewardView,
|
RewardView,
|
||||||
|
RobotView,
|
||||||
StealthView,
|
StealthView,
|
||||||
TickView,
|
TickView,
|
||||||
UserView,
|
UserView,
|
||||||
@ -26,6 +27,7 @@ urlpatterns = [
|
|||||||
OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
|
OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
|
||||||
),
|
),
|
||||||
path("user/", UserView.as_view()),
|
path("user/", UserView.as_view()),
|
||||||
|
path("robot/", RobotView.as_view()),
|
||||||
path("book/", BookView.as_view()),
|
path("book/", BookView.as_view()),
|
||||||
path("info/", InfoView.as_view()),
|
path("info/", InfoView.as_view()),
|
||||||
path("price/", PriceView.as_view()),
|
path("price/", PriceView.as_view()),
|
||||||
|
86
api/utils.py
86
api/utils.py
@ -2,9 +2,11 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import gnupg
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
import ring
|
import ring
|
||||||
|
from base91 import decode, encode
|
||||||
from decouple import config
|
from decouple import config
|
||||||
|
|
||||||
from api.models import Order
|
from api.models import Order
|
||||||
@ -147,18 +149,6 @@ def get_robosats_commit():
|
|||||||
return commit_hash
|
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 = {}
|
premium_percentile = {}
|
||||||
|
|
||||||
|
|
||||||
@ -238,3 +228,75 @@ def compute_avg_premium(queryset):
|
|||||||
else:
|
else:
|
||||||
weighted_median_premium = 0.0
|
weighted_median_premium = 0.0
|
||||||
return weighted_median_premium, total_volume
|
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 django.utils import timezone
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import status, viewsets
|
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.generics import CreateAPIView, ListAPIView, UpdateAPIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from robohash import Robohash
|
from robohash import Robohash
|
||||||
@ -30,6 +35,7 @@ from api.oas_schemas import (
|
|||||||
OrderViewSchema,
|
OrderViewSchema,
|
||||||
PriceViewSchema,
|
PriceViewSchema,
|
||||||
RewardViewSchema,
|
RewardViewSchema,
|
||||||
|
RobotViewSchema,
|
||||||
StealthViewSchema,
|
StealthViewSchema,
|
||||||
TickViewSchema,
|
TickViewSchema,
|
||||||
UserViewSchema,
|
UserViewSchema,
|
||||||
@ -51,7 +57,7 @@ from api.utils import (
|
|||||||
compute_premium_percentile,
|
compute_premium_percentile,
|
||||||
get_lnd_version,
|
get_lnd_version,
|
||||||
get_robosats_commit,
|
get_robosats_commit,
|
||||||
get_robosats_version,
|
validate_pgp_keys,
|
||||||
)
|
)
|
||||||
from chat.models import Message
|
from chat.models import Message
|
||||||
from control.models import AccountingDay, BalanceLog
|
from control.models import AccountingDay, BalanceLog
|
||||||
@ -72,9 +78,12 @@ avatar_path.mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
class MakerView(CreateAPIView):
|
class MakerView(CreateAPIView):
|
||||||
serializer_class = MakeOrderSerializer
|
serializer_class = MakeOrderSerializer
|
||||||
|
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(**MakerViewSchema.post)
|
@extend_schema(**MakerViewSchema.post)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
||||||
serializer = self.serializer_class(data=request.data)
|
serializer = self.serializer_class(data=request.data)
|
||||||
|
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
@ -178,6 +187,8 @@ class MakerView(CreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrderView(viewsets.ViewSet):
|
class OrderView(viewsets.ViewSet):
|
||||||
|
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
serializer_class = UpdateOrderSerializer
|
serializer_class = UpdateOrderSerializer
|
||||||
lookup_url_kwarg = "order_id"
|
lookup_url_kwarg = "order_id"
|
||||||
|
|
||||||
@ -617,13 +628,60 @@ class OrderView(viewsets.ViewSet):
|
|||||||
return self.get(request)
|
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):
|
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(
|
NickGen = NickGenerator(
|
||||||
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer_class = UserGenSerializer
|
serializer_class = UserGenSerializer
|
||||||
|
|
||||||
|
@extend_schema(**UserViewSchema.post)
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
"""
|
"""
|
||||||
Get a new user derived from a high entropy token
|
Get a new user derived from a high entropy token
|
||||||
@ -715,7 +773,7 @@ class UserView(APIView):
|
|||||||
bad_keys_context,
|
bad_keys_context,
|
||||||
public_key,
|
public_key,
|
||||||
encrypted_private_key,
|
encrypted_private_key,
|
||||||
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
) = validate_pgp_keys(public_key, encrypted_private_key)
|
||||||
if not valid:
|
if not valid:
|
||||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
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["lifetime_volume"] = round(lifetime_volume, 8)
|
||||||
context["lnd_version"] = get_lnd_version()
|
context["lnd_version"] = get_lnd_version()
|
||||||
context["robosats_running_commit_hash"] = get_robosats_commit()
|
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_site"] = config("ALTERNATIVE_SITE")
|
||||||
context["alternative_name"] = config("ALTERNATIVE_NAME")
|
context["alternative_name"] = config("ALTERNATIVE_NAME")
|
||||||
context["node_alias"] = config("NODE_ALIAS")
|
context["node_alias"] = config("NODE_ALIAS")
|
||||||
@ -945,18 +1003,15 @@ class InfoView(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RewardView(CreateAPIView):
|
class RewardView(CreateAPIView):
|
||||||
|
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
serializer_class = ClaimRewardSerializer
|
serializer_class = ClaimRewardSerializer
|
||||||
|
|
||||||
@extend_schema(**RewardViewSchema.post)
|
@extend_schema(**RewardViewSchema.post)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = self.serializer_class(data=request.data)
|
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():
|
if not serializer.is_valid():
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -1052,6 +1107,8 @@ class HistoricalView(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class StealthView(UpdateAPIView):
|
class StealthView(UpdateAPIView):
|
||||||
|
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
serializer_class = StealthSerializer
|
serializer_class = StealthSerializer
|
||||||
|
|
||||||
@ -1059,12 +1116,6 @@ class StealthView(UpdateAPIView):
|
|||||||
def put(self, request):
|
def put(self, request):
|
||||||
serializer = self.serializer_class(data=request.data)
|
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():
|
if not serializer.is_valid():
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
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.contrib.auth.models import User
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status, viewsets
|
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 rest_framework.response import Response
|
||||||
|
|
||||||
from api.models import Order
|
from api.models import Order
|
||||||
@ -15,6 +20,9 @@ from chat.serializers import ChatSerializer, PostMessageSerializer
|
|||||||
|
|
||||||
class ChatView(viewsets.ViewSet):
|
class ChatView(viewsets.ViewSet):
|
||||||
serializer_class = PostMessageSerializer
|
serializer_class = PostMessageSerializer
|
||||||
|
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
lookup_url_kwarg = ["order_id", "offset"]
|
lookup_url_kwarg = ["order_id", "offset"]
|
||||||
|
|
||||||
queryset = Message.objects.filter(
|
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",
|
"@mui/x-date-pickers": "^6.0.4",
|
||||||
"@nivo/core": "^0.80.0",
|
"@nivo/core": "^0.80.0",
|
||||||
"@nivo/line": "^0.80.0",
|
"@nivo/line": "^0.80.0",
|
||||||
|
"base-ex": "^0.7.5",
|
||||||
"country-flag-icons": "^1.4.25",
|
"country-flag-icons": "^1.4.25",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"file-replace-loader": "^1.4.0",
|
"file-replace-loader": "^1.4.0",
|
||||||
@ -5165,6 +5166,11 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -18920,6 +18926,11 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"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": {
|
"base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"@mui/x-date-pickers": "^6.0.4",
|
"@mui/x-date-pickers": "^6.0.4",
|
||||||
"@nivo/core": "^0.80.0",
|
"@nivo/core": "^0.80.0",
|
||||||
"@nivo/line": "^0.80.0",
|
"@nivo/line": "^0.80.0",
|
||||||
|
"base-ex": "^0.7.5",
|
||||||
"country-flag-icons": "^1.4.25",
|
"country-flag-icons": "^1.4.25",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"file-replace-loader": "^1.4.0",
|
"file-replace-loader": "^1.4.0",
|
||||||
|
@ -80,7 +80,6 @@ const BookPage = (): JSX.Element => {
|
|||||||
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
|
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
|
||||||
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
|
||||||
<MakerForm
|
<MakerForm
|
||||||
hasRobot={robot.avatarLoaded}
|
|
||||||
onOrderCreated={(id) => {
|
onOrderCreated={(id) => {
|
||||||
navigate('/order/' + id);
|
navigate('/order/' + id);
|
||||||
}}
|
}}
|
||||||
|
@ -77,7 +77,7 @@ const Main: React.FC = () => {
|
|||||||
|
|
||||||
<MainBox navbarHeight={navbarHeight}>
|
<MainBox navbarHeight={navbarHeight}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{['/robot/:refCode?', '/', ''].map((path, index) => {
|
{['/robot/:token?', '/', ''].map((path, index) => {
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
path={path}
|
path={path}
|
||||||
|
@ -100,7 +100,6 @@ const MakerPage = (): JSX.Element => {
|
|||||||
onOrderCreated={(id) => {
|
onOrderCreated={(id) => {
|
||||||
navigate('/order/' + id);
|
navigate('/order/' + id);
|
||||||
}}
|
}}
|
||||||
hasRobot={robot.avatarLoaded}
|
|
||||||
disableRequest={matches.length > 0 && !showMatches}
|
disableRequest={matches.length > 0 && !showMatches}
|
||||||
collapseAll={showMatches}
|
collapseAll={showMatches}
|
||||||
onSubmit={() => setShowMatches(matches.length > 0)}
|
onSubmit={() => setShowMatches(matches.length > 0)}
|
||||||
|
@ -58,7 +58,7 @@ const OrderPage = (): JSX.Element => {
|
|||||||
escrow_duration: order.escrow_duration,
|
escrow_duration: order.escrow_duration,
|
||||||
bond_size: order.bond_size,
|
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) {
|
if (data.bad_request) {
|
||||||
setBadOrder(data.bad_request);
|
setBadOrder(data.bad_request);
|
||||||
} else if (data.id) {
|
} else if (data.id) {
|
||||||
|
@ -11,12 +11,10 @@ import {
|
|||||||
LinearProgress,
|
LinearProgress,
|
||||||
Link,
|
Link,
|
||||||
Typography,
|
Typography,
|
||||||
useTheme,
|
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Page } from '../NavBar';
|
|
||||||
import { Robot } from '../../models';
|
import { Robot } from '../../models';
|
||||||
import { Casino, Bolt, Check, Storefront, AddBox, School } from '@mui/icons-material';
|
import { Casino, Bolt, Check, Storefront, AddBox, School } from '@mui/icons-material';
|
||||||
import RobotAvatar from '../../components/RobotAvatar';
|
import RobotAvatar from '../../components/RobotAvatar';
|
||||||
@ -31,7 +29,7 @@ interface OnboardingProps {
|
|||||||
inputToken: string;
|
inputToken: string;
|
||||||
setInputToken: (state: string) => void;
|
setInputToken: (state: string) => void;
|
||||||
getGenerateRobot: (token: string) => void;
|
getGenerateRobot: (token: string) => void;
|
||||||
badRequest: string | undefined;
|
badToken: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +39,7 @@ const Onboarding = ({
|
|||||||
inputToken,
|
inputToken,
|
||||||
setInputToken,
|
setInputToken,
|
||||||
setRobot,
|
setRobot,
|
||||||
badRequest,
|
badToken,
|
||||||
getGenerateRobot,
|
getGenerateRobot,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
}: OnboardingProps): JSX.Element => {
|
}: OnboardingProps): JSX.Element => {
|
||||||
@ -102,7 +100,7 @@ const Onboarding = ({
|
|||||||
inputToken={inputToken}
|
inputToken={inputToken}
|
||||||
setInputToken={setInputToken}
|
setInputToken={setInputToken}
|
||||||
setRobot={setRobot}
|
setRobot={setRobot}
|
||||||
badRequest={badRequest}
|
badToken={badToken}
|
||||||
robot={robot}
|
robot={robot}
|
||||||
onPressEnter={() => null}
|
onPressEnter={() => null}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { Robot } from '../../models';
|
||||||
import TokenInput from './TokenInput';
|
import TokenInput from './TokenInput';
|
||||||
import Key from '@mui/icons-material/Key';
|
import Key from '@mui/icons-material/Key';
|
||||||
@ -10,6 +10,7 @@ interface RecoveryProps {
|
|||||||
setRobot: (state: Robot) => void;
|
setRobot: (state: Robot) => void;
|
||||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||||
inputToken: string;
|
inputToken: string;
|
||||||
|
badToken: string;
|
||||||
setInputToken: (state: string) => void;
|
setInputToken: (state: string) => void;
|
||||||
getGenerateRobot: (token: string) => void;
|
getGenerateRobot: (token: string) => void;
|
||||||
}
|
}
|
||||||
@ -18,21 +19,16 @@ const Recovery = ({
|
|||||||
robot,
|
robot,
|
||||||
setRobot,
|
setRobot,
|
||||||
inputToken,
|
inputToken,
|
||||||
|
badToken,
|
||||||
setView,
|
setView,
|
||||||
setInputToken,
|
setInputToken,
|
||||||
getGenerateRobot,
|
getGenerateRobot,
|
||||||
}: RecoveryProps): JSX.Element => {
|
}: RecoveryProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const recoveryDisabled = () => {
|
|
||||||
return !(inputToken.length > 20);
|
|
||||||
};
|
|
||||||
const onClickRecover = () => {
|
const onClickRecover = () => {
|
||||||
if (recoveryDisabled()) {
|
|
||||||
} else {
|
|
||||||
getGenerateRobot(inputToken);
|
getGenerateRobot(inputToken);
|
||||||
setView('profile');
|
setView('profile');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,16 +52,11 @@ const Recovery = ({
|
|||||||
label={t('Paste token here')}
|
label={t('Paste token here')}
|
||||||
robot={robot}
|
robot={robot}
|
||||||
onPressEnter={onClickRecover}
|
onPressEnter={onClickRecover}
|
||||||
badRequest={''}
|
badToken={badToken}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button variant='contained' size='large' disabled={!!badToken} onClick={onClickRecover}>
|
||||||
variant='contained'
|
|
||||||
size='large'
|
|
||||||
disabled={recoveryDisabled()}
|
|
||||||
onClick={onClickRecover}
|
|
||||||
>
|
|
||||||
<Key /> <div style={{ width: '0.5em' }} />
|
<Key /> <div style={{ width: '0.5em' }} />
|
||||||
{t('Recover')}
|
{t('Recover')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -27,12 +27,9 @@ interface RobotProfileProps {
|
|||||||
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
|
||||||
getGenerateRobot: (token: string, slot?: number) => void;
|
getGenerateRobot: (token: string, slot?: number) => void;
|
||||||
inputToken: string;
|
inputToken: string;
|
||||||
setCurrentOrder: (state: number) => void;
|
|
||||||
logoutRobot: () => void;
|
logoutRobot: () => void;
|
||||||
inputToken: string;
|
|
||||||
setInputToken: (state: string) => void;
|
setInputToken: (state: string) => void;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
badRequest: string;
|
|
||||||
width: number;
|
width: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +39,8 @@ const RobotProfile = ({
|
|||||||
inputToken,
|
inputToken,
|
||||||
getGenerateRobot,
|
getGenerateRobot,
|
||||||
setInputToken,
|
setInputToken,
|
||||||
setCurrentOrder,
|
|
||||||
logoutRobot,
|
logoutRobot,
|
||||||
setView,
|
setView,
|
||||||
badRequest,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
width,
|
width,
|
||||||
}: RobotProfileProps): JSX.Element => {
|
}: RobotProfileProps): JSX.Element => {
|
||||||
@ -227,7 +222,6 @@ const RobotProfile = ({
|
|||||||
label={t('Store your token safely')}
|
label={t('Store your token safely')}
|
||||||
setInputToken={setInputToken}
|
setInputToken={setInputToken}
|
||||||
setRobot={setRobot}
|
setRobot={setRobot}
|
||||||
badRequest={badRequest}
|
|
||||||
robot={robot}
|
robot={robot}
|
||||||
onPressEnter={() => null}
|
onPressEnter={() => null}
|
||||||
/>
|
/>
|
||||||
|
@ -14,7 +14,7 @@ interface TokenInputProps {
|
|||||||
inputToken: string;
|
inputToken: string;
|
||||||
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
|
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
|
||||||
onPressEnter: () => void;
|
onPressEnter: () => void;
|
||||||
badRequest: string | undefined;
|
badToken?: string;
|
||||||
setInputToken: (state: string) => void;
|
setInputToken: (state: string) => void;
|
||||||
showCopy?: boolean;
|
showCopy?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -30,7 +30,7 @@ const TokenInput = ({
|
|||||||
onPressEnter,
|
onPressEnter,
|
||||||
autoFocusTarget = 'textfield',
|
autoFocusTarget = 'textfield',
|
||||||
inputToken,
|
inputToken,
|
||||||
badRequest,
|
badToken = '',
|
||||||
loading = false,
|
loading = false,
|
||||||
setInputToken,
|
setInputToken,
|
||||||
}: TokenInputProps): JSX.Element => {
|
}: TokenInputProps): JSX.Element => {
|
||||||
@ -46,7 +46,7 @@ const TokenInput = ({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
error={!!badRequest}
|
error={inputToken.length > 20 ? !!badToken : false}
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
required={true}
|
required={true}
|
||||||
label={label || undefined}
|
label={label || undefined}
|
||||||
@ -55,7 +55,7 @@ const TokenInput = ({
|
|||||||
fullWidth={fullWidth}
|
fullWidth={fullWidth}
|
||||||
sx={{ borderColor: 'primary' }}
|
sx={{ borderColor: 'primary' }}
|
||||||
variant={editable ? 'outlined' : 'filled'}
|
variant={editable ? 'outlined' : 'filled'}
|
||||||
helperText={badRequest}
|
helperText={badToken}
|
||||||
size='medium'
|
size='medium'
|
||||||
onChange={(e) => setInputToken(e.target.value)}
|
onChange={(e) => setInputToken(e.target.value)}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={(e) => {
|
||||||
|
@ -20,18 +20,19 @@ import Recovery from './Recovery';
|
|||||||
import { TorIcon } from '../../components/Icons';
|
import { TorIcon } from '../../components/Icons';
|
||||||
import { genKey } from '../../pgp';
|
import { genKey } from '../../pgp';
|
||||||
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||||
|
import { validateTokenEntropy } from '../../utils';
|
||||||
|
|
||||||
const RobotPage = (): JSX.Element => {
|
const RobotPage = (): JSX.Element => {
|
||||||
const { robot, setRobot, setCurrentOrder, fetchRobot, torStatus, windowSize, baseUrl } =
|
const { robot, setRobot, fetchRobot, torStatus, windowSize, baseUrl } =
|
||||||
useContext<UseAppStoreType>(AppContext);
|
useContext<UseAppStoreType>(AppContext);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const refCode = params.refCode;
|
const url_token = params.token;
|
||||||
const width = Math.min(windowSize.width * 0.8, 28);
|
const width = Math.min(windowSize.width * 0.8, 28);
|
||||||
const maxHeight = windowSize.height * 0.85 - 3;
|
const maxHeight = windowSize.height * 0.85 - 3;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [badRequest, setBadRequest] = useState<string | undefined>(undefined);
|
const [badToken, setBadToken] = useState<string>('');
|
||||||
const [inputToken, setInputToken] = useState<string>('');
|
const [inputToken, setInputToken] = useState<string>('');
|
||||||
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
|
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
|
||||||
robot.token ? 'profile' : 'welcome',
|
robot.token ? 'profile' : 'welcome',
|
||||||
@ -41,26 +42,35 @@ const RobotPage = (): JSX.Element => {
|
|||||||
if (robot.token) {
|
if (robot.token) {
|
||||||
setInputToken(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"') {
|
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
|
||||||
fetchRobot({ action: 'generate', setBadRequest });
|
getGenerateRobot(token);
|
||||||
|
setView('profile');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [torStatus]);
|
}, [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) => {
|
const getGenerateRobot = (token: string, slot?: number) => {
|
||||||
setInputToken(token);
|
setInputToken(token);
|
||||||
genKey(token).then(function (key) {
|
genKey(token).then(function (key) {
|
||||||
fetchRobot({
|
fetchRobot({
|
||||||
action: 'generate',
|
|
||||||
newKeys: {
|
newKeys: {
|
||||||
pubKey: key.publicKeyArmored,
|
pubKey: key.publicKeyArmored,
|
||||||
encPrivKey: key.encryptedPrivateKeyArmored,
|
encPrivKey: key.encryptedPrivateKeyArmored,
|
||||||
},
|
},
|
||||||
newToken: token,
|
newToken: token,
|
||||||
slot,
|
slot,
|
||||||
refCode,
|
|
||||||
setBadRequest,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -138,7 +148,7 @@ const RobotPage = (): JSX.Element => {
|
|||||||
setView={setView}
|
setView={setView}
|
||||||
robot={robot}
|
robot={robot}
|
||||||
setRobot={setRobot}
|
setRobot={setRobot}
|
||||||
badRequest={badRequest}
|
badToken={badToken}
|
||||||
inputToken={inputToken}
|
inputToken={inputToken}
|
||||||
setInputToken={setInputToken}
|
setInputToken={setInputToken}
|
||||||
getGenerateRobot={getGenerateRobot}
|
getGenerateRobot={getGenerateRobot}
|
||||||
@ -151,9 +161,6 @@ const RobotPage = (): JSX.Element => {
|
|||||||
setView={setView}
|
setView={setView}
|
||||||
robot={robot}
|
robot={robot}
|
||||||
setRobot={setRobot}
|
setRobot={setRobot}
|
||||||
setCurrentOrder={setCurrentOrder}
|
|
||||||
badRequest={badRequest}
|
|
||||||
getGenerateRobot={getGenerateRobot}
|
|
||||||
logoutRobot={logoutRobot}
|
logoutRobot={logoutRobot}
|
||||||
width={width}
|
width={width}
|
||||||
inputToken={inputToken}
|
inputToken={inputToken}
|
||||||
@ -168,11 +175,10 @@ const RobotPage = (): JSX.Element => {
|
|||||||
setView={setView}
|
setView={setView}
|
||||||
robot={robot}
|
robot={robot}
|
||||||
setRobot={setRobot}
|
setRobot={setRobot}
|
||||||
badRequest={badRequest}
|
badToken={badToken}
|
||||||
inputToken={inputToken}
|
inputToken={inputToken}
|
||||||
setInputToken={setInputToken}
|
setInputToken={setInputToken}
|
||||||
getGenerateRobot={getGenerateRobot}
|
getGenerateRobot={getGenerateRobot}
|
||||||
baseUrl={baseUrl}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -73,10 +73,6 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
|||||||
setWeblnEnabled(webln !== undefined);
|
setWeblnEnabled(webln !== undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const copyReferralCodeHandler = () => {
|
|
||||||
systemClient.copyToClipboard(`http://${host}/robot/${robot.referralCode}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWeblnInvoiceClicked = async (e: any) => {
|
const handleWeblnInvoiceClicked = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (robot.earnedRewards) {
|
if (robot.earnedRewards) {
|
||||||
@ -94,9 +90,14 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
|||||||
setShowRewardsSpinner(true);
|
setShowRewardsSpinner(true);
|
||||||
|
|
||||||
apiClient
|
apiClient
|
||||||
.post(baseUrl, '/api/reward/', {
|
.post(
|
||||||
|
baseUrl,
|
||||||
|
'/api/reward/',
|
||||||
|
{
|
||||||
invoice: rewardInvoice,
|
invoice: rewardInvoice,
|
||||||
})
|
},
|
||||||
|
robot.tokenSHA256,
|
||||||
|
)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
setBadInvoice(data.bad_invoice ?? '');
|
setBadInvoice(data.bad_invoice ?? '');
|
||||||
setShowRewardsSpinner(false);
|
setShowRewardsSpinner(false);
|
||||||
@ -109,7 +110,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
|||||||
|
|
||||||
const setStealthInvoice = (wantsStealth: boolean) => {
|
const setStealthInvoice = (wantsStealth: boolean) => {
|
||||||
apiClient
|
apiClient
|
||||||
.put(baseUrl, '/api/stealth/', { wantsStealth })
|
.put(baseUrl, '/api/stealth/', { wantsStealth }, robot.tokenSHA256)
|
||||||
.then((data) => setRobot({ ...robot, stealthInvoices: data?.wantsStealth }));
|
.then((data) => setRobot({ ...robot, stealthInvoices: data?.wantsStealth }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -268,29 +269,6 @@ const ProfileDialog = ({ open = false, baseUrl, onClose, robot, setRobot }: Prop
|
|||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</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>
|
<ListItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<EmojiEventsIcon />
|
<EmojiEventsIcon />
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Paper } from '@mui/material';
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
@ -28,13 +27,13 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
|
|||||||
this.setState({ hasError: true, error, errorInfo });
|
this.setState({ hasError: true, error, errorInfo });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 10000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div style={{ overflow: 'auto', height: '100%', width: '100%', background: 'white' }}>
|
<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>
|
<p>
|
||||||
<b>Error:</b> {this.state.error.name}
|
<b>Error:</b> {this.state.error.name}
|
||||||
</p>
|
</p>
|
||||||
|
@ -50,7 +50,6 @@ interface MakerFormProps {
|
|||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
submitButtonLabel?: string;
|
submitButtonLabel?: string;
|
||||||
onOrderCreated?: (id: number) => void;
|
onOrderCreated?: (id: number) => void;
|
||||||
hasRobot?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MakerForm = ({
|
const MakerForm = ({
|
||||||
@ -61,9 +60,8 @@ const MakerForm = ({
|
|||||||
onReset = () => {},
|
onReset = () => {},
|
||||||
submitButtonLabel = 'Create Order',
|
submitButtonLabel = 'Create Order',
|
||||||
onOrderCreated = () => null,
|
onOrderCreated = () => null,
|
||||||
hasRobot = true,
|
|
||||||
}: MakerFormProps): JSX.Element => {
|
}: 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);
|
useContext<UseAppStoreType>(AppContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -251,7 +249,7 @@ const MakerForm = ({
|
|||||||
escrow_duration: maker.escrowDuration,
|
escrow_duration: maker.escrowDuration,
|
||||||
bond_size: maker.bondSize,
|
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);
|
setBadRequest(data.bad_request);
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
onOrderCreated(data.id);
|
onOrderCreated(data.id);
|
||||||
@ -466,7 +464,7 @@ const MakerForm = ({
|
|||||||
open={openDialogs}
|
open={openDialogs}
|
||||||
onClose={() => setOpenDialogs(false)}
|
onClose={() => setOpenDialogs(false)}
|
||||||
onClickDone={handleCreateOrder}
|
onClickDone={handleCreateOrder}
|
||||||
hasRobot={hasRobot}
|
hasRobot={robot.avatarLoaded}
|
||||||
/>
|
/>
|
||||||
<Collapse in={limits.list.length == 0}>
|
<Collapse in={limits.list.length == 0}>
|
||||||
<div style={{ display: limits.list.length == 0 ? '' : 'none' }}>
|
<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 { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -21,16 +21,16 @@ import Countdown from 'react-countdown';
|
|||||||
import currencies from '../../../static/assets/currencies.json';
|
import currencies from '../../../static/assets/currencies.json';
|
||||||
import { apiClient } from '../../services/api';
|
import { apiClient } from '../../services/api';
|
||||||
|
|
||||||
import { Order } from '../../models';
|
import { Order, Info } from '../../models';
|
||||||
import { ConfirmationDialog } from '../Dialogs';
|
import { ConfirmationDialog } from '../Dialogs';
|
||||||
import { LoadingButton } from '@mui/lab';
|
import { LoadingButton } from '@mui/lab';
|
||||||
import { computeSats, pn } from '../../utils';
|
import { computeSats } from '../../utils';
|
||||||
|
import { AppContext, UseAppStoreType } from '../../contexts/AppContext';
|
||||||
|
|
||||||
interface TakeButtonProps {
|
interface TakeButtonProps {
|
||||||
order: Order;
|
order: Order;
|
||||||
setOrder: (state: Order) => void;
|
setOrder: (state: Order) => void;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
hasRobot: boolean;
|
|
||||||
info: Info;
|
info: Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,9 +40,10 @@ interface OpenDialogsProps {
|
|||||||
}
|
}
|
||||||
const closeAll = { inactiveMaker: false, confirmation: false };
|
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 { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { robot } = useContext<UseAppStoreType>(AppContext);
|
||||||
|
|
||||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||||
const [badRequest, setBadRequest] = useState<string>('');
|
const [badRequest, setBadRequest] = useState<string>('');
|
||||||
@ -277,10 +278,15 @@ const TakeButton = ({ order, setOrder, baseUrl, hasRobot, info }: TakeButtonProp
|
|||||||
const takeOrder = function () {
|
const takeOrder = function () {
|
||||||
setLoadingTake(true);
|
setLoadingTake(true);
|
||||||
apiClient
|
apiClient
|
||||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
.post(
|
||||||
|
baseUrl,
|
||||||
|
'/api/order/?order_id=' + order.id,
|
||||||
|
{
|
||||||
action: 'take',
|
action: 'take',
|
||||||
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
amount: order.currency == 1000 ? takeAmount / 100000000 : takeAmount,
|
||||||
})
|
},
|
||||||
|
robot.tokenSHA256,
|
||||||
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setLoadingTake(false);
|
setLoadingTake(false);
|
||||||
if (data.bad_request) {
|
if (data.bad_request) {
|
||||||
@ -313,7 +319,7 @@ const TakeButton = ({ order, setOrder, baseUrl, hasRobot, info }: TakeButtonProp
|
|||||||
setLoadingTake(true);
|
setLoadingTake(true);
|
||||||
setOpen(closeAll);
|
setOpen(closeAll);
|
||||||
}}
|
}}
|
||||||
hasRobot={hasRobot}
|
hasRobot={robot.avatarLoaded}
|
||||||
/>
|
/>
|
||||||
<InactiveMakerDialog />
|
<InactiveMakerDialog />
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -14,7 +14,7 @@ interface Props {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StringAsIcons: React.FC = ({ othersText, verbose, size, text }: Props) => {
|
const StringAsIcons: React.FC = ({ othersText, verbose, size, text = '' }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const parsedText = useMemo(() => {
|
const parsedText = useMemo(() => {
|
||||||
|
@ -14,6 +14,7 @@ import MessageCard from '../MessageCard';
|
|||||||
import ChatHeader from '../ChatHeader';
|
import ChatHeader from '../ChatHeader';
|
||||||
import { EncryptedChatMessage, ServerMessage } from '..';
|
import { EncryptedChatMessage, ServerMessage } from '..';
|
||||||
import ChatBottom from '../ChatBottom';
|
import ChatBottom from '../ChatBottom';
|
||||||
|
import { sha256 } from 'js-sha256';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
@ -92,13 +93,18 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
}, [serverMessages]);
|
}, [serverMessages]);
|
||||||
|
|
||||||
const connectWebsocket = () => {
|
const connectWebsocket = () => {
|
||||||
websocketClient.open(`ws://${window.location.host}/ws/chat/${orderId}/`).then((connection) => {
|
websocketClient
|
||||||
|
.open(
|
||||||
|
`ws://${window.location.host}/ws/chat/${orderId}/?token_sha256_hex=${sha256(robot.token)}`,
|
||||||
|
)
|
||||||
|
.then((connection) => {
|
||||||
setConnection(connection);
|
setConnection(connection);
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
|
|
||||||
connection.send({
|
connection.send({
|
||||||
message: robot.pubKey,
|
message: robot.pubKey,
|
||||||
nick: userNick,
|
nick: userNick,
|
||||||
|
authorization: `Token ${robot.tokenSHA256}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onMessage((message) => setServerMessages((prev) => [...prev, message]));
|
connection.onMessage((message) => setServerMessages((prev) => [...prev, message]));
|
||||||
@ -135,6 +141,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
connection.send({
|
connection.send({
|
||||||
message: `-----SERVE HISTORY-----`,
|
message: `-----SERVE HISTORY-----`,
|
||||||
nick: userNick,
|
nick: userNick,
|
||||||
|
authorization: `Token ${robot.tokenSHA256}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// If we receive an encrypted message
|
// If we receive an encrypted message
|
||||||
@ -206,6 +213,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
connection.send({
|
connection.send({
|
||||||
message: value,
|
message: value,
|
||||||
nick: userNick,
|
nick: userNick,
|
||||||
|
authorization: `Token ${robot.tokenSHA256}`,
|
||||||
});
|
});
|
||||||
setValue('');
|
setValue('');
|
||||||
}
|
}
|
||||||
@ -221,6 +229,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
|
|||||||
connection.send({
|
connection.send({
|
||||||
message: encryptedMessage.toString().split('\n').join('\\'),
|
message: encryptedMessage.toString().split('\n').join('\\'),
|
||||||
nick: userNick,
|
nick: userNick,
|
||||||
|
authorization: `Token ${robot.tokenSHA256}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -76,7 +76,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
|
|
||||||
const loadMessages: () => void = () => {
|
const loadMessages: () => void = () => {
|
||||||
apiClient
|
apiClient
|
||||||
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`)
|
.get(baseUrl, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, robot.tokenSHA256)
|
||||||
.then((results: any) => {
|
.then((results: any) => {
|
||||||
if (results) {
|
if (results) {
|
||||||
setPeerConnected(results.peer_connected);
|
setPeerConnected(results.peer_connected);
|
||||||
@ -167,11 +167,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
// If input string contains '#' send unencrypted and unlogged message
|
// If input string contains '#' send unencrypted and unlogged message
|
||||||
else if (value.substring(0, 1) == '#') {
|
else if (value.substring(0, 1) == '#') {
|
||||||
apiClient
|
apiClient
|
||||||
.post(baseUrl, `/api/chat/`, {
|
.post(
|
||||||
|
baseUrl,
|
||||||
|
`/api/chat/`,
|
||||||
|
{
|
||||||
PGP_message: value,
|
PGP_message: value,
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
offset: lastIndex,
|
offset: lastIndex,
|
||||||
})
|
},
|
||||||
|
robot.tokenSHA256,
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
if (response.messages) {
|
if (response.messages) {
|
||||||
@ -192,11 +197,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
|
|||||||
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token)
|
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token)
|
||||||
.then((encryptedMessage) => {
|
.then((encryptedMessage) => {
|
||||||
apiClient
|
apiClient
|
||||||
.post(baseUrl, `/api/chat/`, {
|
.post(
|
||||||
|
baseUrl,
|
||||||
|
`/api/chat/`,
|
||||||
|
{
|
||||||
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
PGP_message: encryptedMessage.toString().split('\n').join('\\'),
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
offset: lastIndex,
|
offset: lastIndex,
|
||||||
})
|
},
|
||||||
|
robot.tokenSHA256,
|
||||||
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
setPeerConnected(response.peer_connected);
|
setPeerConnected(response.peer_connected);
|
||||||
|
@ -161,7 +161,10 @@ const TradeBox = ({
|
|||||||
rating,
|
rating,
|
||||||
}: SubmitActionProps) {
|
}: SubmitActionProps) {
|
||||||
apiClient
|
apiClient
|
||||||
.post(baseUrl, '/api/order/?order_id=' + order.id, {
|
.post(
|
||||||
|
baseUrl,
|
||||||
|
'/api/order/?order_id=' + order.id,
|
||||||
|
{
|
||||||
action,
|
action,
|
||||||
invoice,
|
invoice,
|
||||||
routing_budget_ppm,
|
routing_budget_ppm,
|
||||||
@ -169,7 +172,9 @@ const TradeBox = ({
|
|||||||
mining_fee_rate,
|
mining_fee_rate,
|
||||||
statement,
|
statement,
|
||||||
rating,
|
rating,
|
||||||
})
|
},
|
||||||
|
robot.tokenSHA256,
|
||||||
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setOpen(closeAll);
|
setOpen(closeAll);
|
||||||
setLoadingButtons({ ...noLoadingButtons });
|
setLoadingButtons({ ...noLoadingButtons });
|
||||||
|
@ -18,13 +18,13 @@ import {
|
|||||||
} from '../models';
|
} from '../models';
|
||||||
|
|
||||||
import { apiClient } from '../services/api';
|
import { apiClient } from '../services/api';
|
||||||
import { systemClient } from '../services/System';
|
import { checkVer, getHost, hexToBase91, validateTokenEntropy } from '../utils';
|
||||||
import { checkVer, getHost, tokenStrength } from '../utils';
|
|
||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
|
|
||||||
import defaultCoordinators from '../../static/federation.json';
|
import defaultCoordinators from '../../static/federation.json';
|
||||||
import { createTheme, Theme } from '@mui/material/styles';
|
import { createTheme, Theme } from '@mui/material/styles';
|
||||||
import i18n from '../i18n/Web';
|
import i18n from '../i18n/Web';
|
||||||
|
import { systemClient } from '../services/System';
|
||||||
|
|
||||||
const getWindowSize = function (fontSize: number) {
|
const getWindowSize = function (fontSize: number) {
|
||||||
// returns window size in EM units
|
// returns window size in EM units
|
||||||
@ -63,12 +63,10 @@ export interface SlideDirection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface fetchRobotProps {
|
export interface fetchRobotProps {
|
||||||
action?: 'login' | 'generate' | 'refresh';
|
newKeys?: { encPrivKey: string; pubKey: string };
|
||||||
newKeys?: { encPrivKey: string; pubKey: string } | null;
|
newToken?: string;
|
||||||
newToken?: string | null;
|
slot?: number;
|
||||||
refCode?: string | null;
|
isRefresh?: boolean;
|
||||||
slot?: number | null;
|
|
||||||
setBadRequest?: (state: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
||||||
@ -297,7 +295,9 @@ export const useAppStore = () => {
|
|||||||
|
|
||||||
const fetchOrder = function () {
|
const fetchOrder = function () {
|
||||||
if (currentOrder != undefined) {
|
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,37 +307,62 @@ export const useAppStore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchRobot = function ({
|
const fetchRobot = function ({
|
||||||
action = 'login',
|
newToken,
|
||||||
newKeys = null,
|
newKeys,
|
||||||
newToken = null,
|
slot,
|
||||||
refCode = null,
|
isRefresh = false,
|
||||||
slot = null,
|
}: fetchRobotProps): void {
|
||||||
setBadRequest = () => {},
|
const token = newToken ?? robot.token ?? '';
|
||||||
}: fetchRobotProps) {
|
|
||||||
const oldRobot = robot;
|
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
|
||||||
|
|
||||||
|
if (!hasEnoughEntropy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenSHA256 = hexToBase91(sha256(token));
|
||||||
const targetSlot = slot ?? currentSlot;
|
const targetSlot = slot ?? currentSlot;
|
||||||
const token = newToken ?? oldRobot.token;
|
const encPrivKey = newKeys?.encPrivKey ?? robot.encPrivKey ?? '';
|
||||||
if (action != 'refresh') {
|
const pubKey = newKeys?.pubKey ?? robot.pubKey ?? '';
|
||||||
setRobot(new Robot());
|
|
||||||
}
|
|
||||||
setBadRequest('');
|
|
||||||
|
|
||||||
const requestBody = {};
|
// On first authenticated request, pubkey and privkey must be in header cookies
|
||||||
if (action == 'login' || action == 'refresh') {
|
systemClient.setCookie('public_key', pubKey.split('\n').join('\\'));
|
||||||
requestBody.token_sha256 = sha256(token);
|
systemClient.setCookie('encrypted_private_key', encPrivKey.split('\n').join('\\'));
|
||||||
} else if (action == 'generate' && token != null) {
|
|
||||||
const strength = tokenStrength(token);
|
if (!isRefresh) {
|
||||||
requestBody.token_sha256 = sha256(token);
|
setRobot((robot) => {
|
||||||
requestBody.unique_values = strength.uniqueValues;
|
return {
|
||||||
requestBody.counts = strength.counts;
|
...robot,
|
||||||
requestBody.length = token.length;
|
loading: true,
|
||||||
requestBody.ref_code = refCode;
|
avatarLoaded: false,
|
||||||
requestBody.public_key = newKeys?.pubKey ?? oldRobot.pubKey;
|
};
|
||||||
requestBody.encrypted_private_key = newKeys?.encPrivKey ?? oldRobot.encPrivKey;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
|
apiClient
|
||||||
let newRobot = robot;
|
.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,
|
||||||
|
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,
|
||||||
|
last_login: data.last_login,
|
||||||
|
bitsEntropy,
|
||||||
|
shannonEntropy,
|
||||||
|
pubKey: data.public_key,
|
||||||
|
encPrivKey: data.encrypted_private_key,
|
||||||
|
copiedToken: !!data.found,
|
||||||
|
};
|
||||||
if (currentOrder === undefined) {
|
if (currentOrder === undefined) {
|
||||||
setCurrentOrder(
|
setCurrentOrder(
|
||||||
data.active_order_id
|
data.active_order_id
|
||||||
@ -347,57 +372,22 @@ export const useAppStore = () => {
|
|||||||
: null,
|
: 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,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
newRobot = {
|
|
||||||
...oldRobot,
|
|
||||||
nickname: data.nickname,
|
|
||||||
token,
|
|
||||||
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,
|
|
||||||
pubKey: data.public_key,
|
|
||||||
encPrivKey: data.encrypted_private_key,
|
|
||||||
copiedToken: !!data.found,
|
|
||||||
};
|
|
||||||
setRobot(newRobot);
|
setRobot(newRobot);
|
||||||
garage.updateRobot(newRobot, targetSlot);
|
garage.updateRobot(newRobot, targetSlot);
|
||||||
setCurrentSlot(targetSlot);
|
setCurrentSlot(targetSlot);
|
||||||
systemClient.setItem('robot_token', token);
|
})
|
||||||
}
|
.finally(() => {
|
||||||
|
systemClient.deleteCookie('public_key');
|
||||||
|
systemClient.deleteCookie('encrypted_private_key');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (baseUrl != '' && page != 'robot') {
|
if (baseUrl != '' && page != 'robot') {
|
||||||
if (open.profile && robot.avatarLoaded) {
|
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) {
|
} 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]);
|
}, [open.profile, baseUrl]);
|
||||||
|
@ -2,6 +2,7 @@ class Robot {
|
|||||||
constructor(garageRobot?: Robot) {
|
constructor(garageRobot?: Robot) {
|
||||||
if (garageRobot != null) {
|
if (garageRobot != null) {
|
||||||
this.token = garageRobot?.token ?? undefined;
|
this.token = garageRobot?.token ?? undefined;
|
||||||
|
this.tokenSHA256 = garageRobot?.tokenSHA256 ?? undefined;
|
||||||
this.pubKey = garageRobot?.pubKey ?? undefined;
|
this.pubKey = garageRobot?.pubKey ?? undefined;
|
||||||
this.encPrivKey = garageRobot?.encPrivKey ?? undefined;
|
this.encPrivKey = garageRobot?.encPrivKey ?? undefined;
|
||||||
}
|
}
|
||||||
@ -9,20 +10,21 @@ class Robot {
|
|||||||
|
|
||||||
public nickname?: string;
|
public nickname?: string;
|
||||||
public token?: string;
|
public token?: string;
|
||||||
public pubKey?: string;
|
|
||||||
public encPrivKey?: string;
|
|
||||||
public bitsEntropy?: number;
|
public bitsEntropy?: number;
|
||||||
public shannonEntropy?: number;
|
public shannonEntropy?: number;
|
||||||
|
public tokenSHA256?: string;
|
||||||
|
public pubKey?: string;
|
||||||
|
public encPrivKey?: string;
|
||||||
public stealthInvoices: boolean = true;
|
public stealthInvoices: boolean = true;
|
||||||
public activeOrderId?: number;
|
public activeOrderId?: number;
|
||||||
public lastOrderId?: number;
|
public lastOrderId?: number;
|
||||||
public earnedRewards: number = 0;
|
public earnedRewards: number = 0;
|
||||||
public referralCode: string = '';
|
|
||||||
public tgEnabled: boolean = false;
|
public tgEnabled: boolean = false;
|
||||||
public tgBotName: string = 'unknown';
|
public tgBotName: string = 'unknown';
|
||||||
public tgToken: string = 'unknown';
|
public tgToken: string = 'unknown';
|
||||||
public loading: boolean = false;
|
public loading: boolean = false;
|
||||||
public found: boolean = false;
|
public found: boolean = false;
|
||||||
|
public last_login: string = '';
|
||||||
public avatarLoaded: boolean = false;
|
public avatarLoaded: boolean = false;
|
||||||
public copiedToken: boolean = false;
|
public copiedToken: boolean = false;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class SystemWebClient implements SystemClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public deleteCookie: (key: string) => void = (key) => {
|
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
|
// Local storage
|
||||||
|
@ -5,21 +5,27 @@ class ApiNativeClient implements ApiClient {
|
|||||||
private assetsCache: { [path: string]: string } = {};
|
private assetsCache: { [path: string]: string } = {};
|
||||||
private assetsPromises: { [path: string]: Promise<string | undefined> } = {};
|
private assetsPromises: { [path: string]: Promise<string | undefined> } = {};
|
||||||
|
|
||||||
private readonly getHeaders: () => HeadersInit = () => {
|
private readonly getHeaders: (tokenSHA256?: string) => HeadersInit = (tokenSHA256) => {
|
||||||
let headers = {
|
let headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
const robotToken = systemClient.getItem('robot_token');
|
if (tokenSHA256) {
|
||||||
if (robotToken) {
|
|
||||||
const sessionid = systemClient.getCookie('sessionid');
|
|
||||||
// const csrftoken = systemClient.getCookie('csrftoken');
|
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
...headers,
|
...headers,
|
||||||
...{
|
...{
|
||||||
// 'X-CSRFToken': csrftoken,
|
Authorization: `Token ${tokenSHA256.substring(0, 40)}`,
|
||||||
Cookie: `sessionid=${sessionid}`, // ;csrftoken=${csrftoken}
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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,41 +51,44 @@ class ApiNativeClient implements ApiClient {
|
|||||||
return await new Promise((res, _rej) => res({}));
|
return await new Promise((res, _rej) => res({}));
|
||||||
};
|
};
|
||||||
|
|
||||||
public delete: (baseUrl: string, path: string) => Promise<object | undefined> = async (
|
public delete: (
|
||||||
baseUrl,
|
baseUrl: string,
|
||||||
path,
|
path: string,
|
||||||
) => {
|
tokenSHA256?: string,
|
||||||
|
) => Promise<object | undefined> = async (baseUrl, path, tokenSHA256) => {
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
}).then(this.parseResponse);
|
}).then(this.parseResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
public post: (baseUrl: string, path: string, body: object) => Promise<object | undefined> =
|
public post: (
|
||||||
async (baseUrl, path, body) => {
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
body: object,
|
||||||
|
tokenSHA256?: string,
|
||||||
|
) => Promise<object | undefined> = async (baseUrl, path, body, tokenSHA256) => {
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
body,
|
body,
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
}).then(this.parseResponse);
|
}).then(this.parseResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
public get: (baseUrl: string, path: string) => Promise<object | undefined> = async (
|
public get: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object | undefined> =
|
||||||
baseUrl,
|
async (baseUrl, path, tokenSHA256) => {
|
||||||
path,
|
|
||||||
) => {
|
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'get',
|
type: 'get',
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
}).then(this.parseResponse);
|
}).then(this.parseResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,22 +1,32 @@
|
|||||||
import { ApiClient } from '..';
|
import { ApiClient } from '..';
|
||||||
import { systemClient } from '../../System';
|
|
||||||
|
|
||||||
class ApiWebClient implements ApiClient {
|
class ApiWebClient implements ApiClient {
|
||||||
private readonly getHeaders: () => HeadersInit = () => {
|
private readonly getHeaders: (tokenSHA256?: string) => HeadersInit = (tokenSHA256) => {
|
||||||
return {
|
let headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// 'X-CSRFToken': systemClient.getCookie('csrftoken') || '',
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public post: (baseUrl: string, path: string, body: object) => Promise<object> = async (
|
if (tokenSHA256) {
|
||||||
baseUrl,
|
headers = {
|
||||||
path,
|
...headers,
|
||||||
body,
|
...{
|
||||||
) => {
|
Authorization: `Token ${tokenSHA256.substring(0, 40)}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
public post: (
|
||||||
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
body: object,
|
||||||
|
tokenSHA256?: string,
|
||||||
|
) => Promise<object> = async (baseUrl, path, body, tokenSHA256) => {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,14 +35,15 @@ class ApiWebClient implements ApiClient {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public put: (baseUrl: string, path: string, body: object) => Promise<object> = async (
|
public put: (
|
||||||
baseUrl,
|
baseUrl: string,
|
||||||
path,
|
path: string,
|
||||||
body,
|
body: object,
|
||||||
) => {
|
tokenSHA256?: string,
|
||||||
|
) => Promise<object> = async (baseUrl, path, body, tokenSHA256) => {
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
};
|
};
|
||||||
return await fetch(baseUrl + path, requestOptions).then(
|
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 = {
|
const requestOptions = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(tokenSHA256),
|
||||||
};
|
};
|
||||||
return await fetch(baseUrl + path, requestOptions).then(
|
return await fetch(baseUrl + path, requestOptions).then(
|
||||||
async (response) => await response.json(),
|
async (response) => await response.json(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public get: (baseUrl: string, path: string) => Promise<object> = async (baseUrl, path) => {
|
public get: (baseUrl: string, path: string, tokenSHA256?: string) => Promise<object> = async (
|
||||||
return await fetch(baseUrl + path).then(async (response) => await response.json());
|
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';
|
import ApiNativeClient from './ApiNativeClient';
|
||||||
|
|
||||||
export interface ApiClient {
|
export interface ApiClient {
|
||||||
post: (baseUrl: string, path: string, body: object) => Promise<object | undefined>;
|
post: (
|
||||||
put: (baseUrl: string, path: string, body: object) => Promise<object | undefined>;
|
baseUrl: string,
|
||||||
get: (baseUrl: string, path: string) => Promise<object | undefined>;
|
path: string,
|
||||||
delete: (baseUrl: string, path: string) => Promise<object | undefined>;
|
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>;
|
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 filterOrders } from './filterOrders';
|
||||||
export { default as getHost } from './getHost';
|
export { default as getHost } from './getHost';
|
||||||
export { default as hexToRgb } from './hexToRgb';
|
export { default as hexToRgb } from './hexToRgb';
|
||||||
|
export { default as hexToBase91 } from './hexToBase91';
|
||||||
export { default as matchMedian } from './match';
|
export { default as matchMedian } from './match';
|
||||||
export { default as pn } from './prettyNumbers';
|
export { default as pn } from './prettyNumbers';
|
||||||
export { amountToString } from './prettyNumbers';
|
export { amountToString } from './prettyNumbers';
|
||||||
export { default as saveAsJson } from './saveFile';
|
export { default as saveAsJson } from './saveFile';
|
||||||
export { default as statusBadgeColor } from './statusBadgeColor';
|
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 getWebln } from './webln';
|
||||||
export { default as computeSats } from './computeSats';
|
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",
|
"Learn RoboSats": "Aprèn RoboSats",
|
||||||
"See profile": "Veure perfil",
|
"See profile": "Veure perfil",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connectant a TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connexió xifrada i anònima mitjançant 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ó.",
|
"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",
|
"Telegram enabled": "Telegram activat",
|
||||||
"Enable Telegram Notifications": "Notificar en Telegram",
|
"Enable Telegram Notifications": "Notificar en Telegram",
|
||||||
"Use stealth invoices": "Factures ofuscades",
|
"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",
|
"Your earned rewards": "Les teves recompenses guanyades",
|
||||||
"Claim": "Retirar",
|
"Claim": "Retirar",
|
||||||
"Invoice for {{amountSats}} Sats": "Factura per {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Factura per {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Více o RoboSats",
|
"Learn RoboSats": "Více o RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram povolen",
|
||||||
"Enable Telegram Notifications": "Povolit Telegram notifikace",
|
"Enable Telegram Notifications": "Povolit Telegram notifikace",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Tvé odměny",
|
||||||
"Claim": "Vybrat",
|
"Claim": "Vybrat",
|
||||||
"Invoice for {{amountSats}} Sats": "Invoice pro {{amountSats}} Satů",
|
"Invoice for {{amountSats}} Sats": "Invoice pro {{amountSats}} Satů",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Lerne RoboSats kennen",
|
"Learn RoboSats": "Lerne RoboSats kennen",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram aktiviert",
|
||||||
"Enable Telegram Notifications": "Telegram-Benachrichtigungen aktivieren",
|
"Enable Telegram Notifications": "Telegram-Benachrichtigungen aktivieren",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Deine verdienten Belohnungen",
|
||||||
"Claim": "Erhalten",
|
"Claim": "Erhalten",
|
||||||
"Invoice for {{amountSats}} Sats": "Invoice für {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Invoice für {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Learn RoboSats",
|
"Learn RoboSats": "Learn RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram enabled",
|
||||||
"Enable Telegram Notifications": "Enable Telegram Notifications",
|
"Enable Telegram Notifications": "Enable Telegram Notifications",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Your earned rewards",
|
||||||
"Claim": "Claim",
|
"Claim": "Claim",
|
||||||
"Invoice for {{amountSats}} Sats": "Invoice for {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Invoice for {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Aprende RoboSats",
|
"Learn RoboSats": "Aprende RoboSats",
|
||||||
"See profile": "Ver perfil",
|
"See profile": "Ver perfil",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Conectando con TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Conexión encriptada y anonimizada usando 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.",
|
"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",
|
"Telegram enabled": "Telegram activado",
|
||||||
"Enable Telegram Notifications": "Notificar en Telegram",
|
"Enable Telegram Notifications": "Notificar en Telegram",
|
||||||
"Use stealth invoices": "Facturas sigilosas",
|
"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",
|
"Your earned rewards": "Tus recompensas ganadas",
|
||||||
"Claim": "Reclamar",
|
"Claim": "Reclamar",
|
||||||
"Invoice for {{amountSats}} Sats": "Factura de {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Factura de {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Ikasi RoboSats",
|
"Learn RoboSats": "Ikasi RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram baimendua",
|
||||||
"Enable Telegram Notifications": "Baimendu Telegram Jakinarazpenak",
|
"Enable Telegram Notifications": "Baimendu Telegram Jakinarazpenak",
|
||||||
"Use stealth invoices": "Erabili ezkutuko fakturak",
|
"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",
|
"Your earned rewards": "Irabazitako sariak",
|
||||||
"Claim": "Eskatu",
|
"Claim": "Eskatu",
|
||||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} Sateko fakura",
|
"Invoice for {{amountSats}} Sats": "{{amountSats}} Sateko fakura",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Learn RoboSats",
|
"Learn RoboSats": "Learn RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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é",
|
"Telegram enabled": "Telegram activé",
|
||||||
"Enable Telegram Notifications": "Activer les notifications Telegram",
|
"Enable Telegram Notifications": "Activer les notifications Telegram",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Vos récompenses gagnées",
|
||||||
"Claim": "Réclamer",
|
"Claim": "Réclamer",
|
||||||
"Invoice for {{amountSats}} Sats": "Facture pour {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Facture pour {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Impara RoboSats",
|
"Learn RoboSats": "Impara RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram attivato",
|
||||||
"Enable Telegram Notifications": "Attiva notifiche Telegram",
|
"Enable Telegram Notifications": "Attiva notifiche Telegram",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "La tua ricompensa",
|
||||||
"Claim": "Riscatta",
|
"Claim": "Riscatta",
|
||||||
"Invoice for {{amountSats}} Sats": "Ricevuta per {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Ricevuta per {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Robosatsを学ぶ",
|
"Learn RoboSats": "Robosatsを学ぶ",
|
||||||
"See profile": "プロフィール",
|
"See profile": "プロフィール",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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ネットワークに接続中",
|
"Connecting to TOR": "Torネットワークに接続中",
|
||||||
"Connection encrypted and anonymized using 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.": "これにより最大限のプライバシーが確保されますが、アプリの動作が遅いと感じることがあります。接続が切断された場合は、アプリを再起動してください。",
|
"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が有効になりました",
|
"Telegram enabled": "Telegramが有効になりました",
|
||||||
"Enable Telegram Notifications": "Telegram通知を有効にする",
|
"Enable Telegram Notifications": "Telegram通知を有効にする",
|
||||||
"Use stealth invoices": "ステルス・インボイスを使用する",
|
"Use stealth invoices": "ステルス・インボイスを使用する",
|
||||||
"Share to earn 100 Sats per trade": "共有して取引ごとに100 Satsを稼ぐ",
|
|
||||||
"Your referral link": "あなたの紹介リンク",
|
|
||||||
"Your earned rewards": "あなたの獲得報酬",
|
"Your earned rewards": "あなたの獲得報酬",
|
||||||
"Claim": "請求する",
|
"Claim": "請求する",
|
||||||
"Invoice for {{amountSats}} Sats": " {{amountSats}} Satsのインボイス",
|
"Invoice for {{amountSats}} Sats": " {{amountSats}} Satsのインボイス",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Learn RoboSats",
|
"Learn RoboSats": "Learn RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram włączony",
|
||||||
"Enable Telegram Notifications": "Włącz powiadomienia telegramu",
|
"Enable Telegram Notifications": "Włącz powiadomienia telegramu",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Twoje zarobione nagrody",
|
||||||
"Claim": "Prawo",
|
"Claim": "Prawo",
|
||||||
"Invoice for {{amountSats}} Sats": "Faktura za {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Faktura za {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Aprender sobre o RoboSats",
|
"Learn RoboSats": "Aprender sobre o RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram ativado",
|
||||||
"Enable Telegram Notifications": "Habilitar notificações do Telegram",
|
"Enable Telegram Notifications": "Habilitar notificações do Telegram",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Suas recompensas ganhas",
|
||||||
"Claim": "Reinvindicar",
|
"Claim": "Reinvindicar",
|
||||||
"Invoice for {{amountSats}} Sats": "Invoice para {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Invoice para {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Изучить RoboSats",
|
"Learn RoboSats": "Изучить RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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 включен",
|
"Telegram enabled": "Telegram включен",
|
||||||
"Enable Telegram Notifications": "Включить уведомления Telegram",
|
"Enable Telegram Notifications": "Включить уведомления Telegram",
|
||||||
"Use stealth invoices": "Использовать стелс инвойсы",
|
"Use stealth invoices": "Использовать стелс инвойсы",
|
||||||
"Share to earn 100 Sats per trade": "Поделись, чтобы заработать 100 Сатоши за сделку",
|
|
||||||
"Your referral link": "Ваша реферальная ссылка",
|
|
||||||
"Your earned rewards": "Ваши заработанные награды",
|
"Your earned rewards": "Ваши заработанные награды",
|
||||||
"Claim": "Запросить",
|
"Claim": "Запросить",
|
||||||
"Invoice for {{amountSats}} Sats": "Инвойс на {{amountSats}} Сатоши",
|
"Invoice for {{amountSats}} Sats": "Инвойс на {{amountSats}} Сатоши",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "Learn RoboSats",
|
"Learn RoboSats": "Learn RoboSats",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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",
|
"Telegram enabled": "Telegram aktiverat",
|
||||||
"Enable Telegram Notifications": "Aktivera Telegram-notiser",
|
"Enable Telegram Notifications": "Aktivera Telegram-notiser",
|
||||||
"Use stealth invoices": "Use stealth invoices",
|
"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",
|
"Your earned rewards": "Du fick belöningar",
|
||||||
"Claim": "Claim",
|
"Claim": "Claim",
|
||||||
"Invoice for {{amountSats}} Sats": "Faktura för {{amountSats}} sats",
|
"Invoice for {{amountSats}} Sats": "Faktura för {{amountSats}} sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "เรียนรู้การใช้งาน",
|
"Learn RoboSats": "เรียนรู้การใช้งาน",
|
||||||
"See profile": "See profile",
|
"See profile": "See profile",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "Connecting to TOR",
|
||||||
"Connection encrypted and anonymized using TOR.": "Connection encrypted and anonymized using 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.",
|
"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 แล้ว",
|
"Telegram enabled": "เปิดใช้ Telegram แล้ว",
|
||||||
"Enable Telegram Notifications": "เปิดใช้การแจ้งเตือนผ่านทาง Telegram",
|
"Enable Telegram Notifications": "เปิดใช้การแจ้งเตือนผ่านทาง Telegram",
|
||||||
"Use stealth invoices": "ใช้งาน stealth invoices",
|
"Use stealth invoices": "ใช้งาน stealth invoices",
|
||||||
"Share to earn 100 Sats per trade": "Share เพื่อรับ 100 Sats ต่อการซื้อขาย",
|
|
||||||
"Your referral link": "ลิ้งค์ referral ของคุณ",
|
|
||||||
"Your earned rewards": "รางวัลที่ตุณได้รับ",
|
"Your earned rewards": "รางวัลที่ตุณได้รับ",
|
||||||
"Claim": "รับรางวัล",
|
"Claim": "รับรางวัล",
|
||||||
"Invoice for {{amountSats}} Sats": "Invoice สำหรับ {{amountSats}} Sats",
|
"Invoice for {{amountSats}} Sats": "Invoice สำหรับ {{amountSats}} Sats",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "学习 RoboSats",
|
"Learn RoboSats": "学习 RoboSats",
|
||||||
"See profile": "查看个人资料",
|
"See profile": "查看个人资料",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "正在连线 TOR",
|
||||||
"Connection encrypted and anonymized using 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.": "这确保最高的隐秘程度,但你可能会觉得应用程序运作缓慢。如果丢失连接,请重启应用。",
|
"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 已开启",
|
"Telegram enabled": "Telegram 已开启",
|
||||||
"Enable Telegram Notifications": "开启 Telegram 通知",
|
"Enable Telegram Notifications": "开启 Telegram 通知",
|
||||||
"Use stealth invoices": "使用隐形发票",
|
"Use stealth invoices": "使用隐形发票",
|
||||||
"Share to earn 100 Sats per trade": "分享并于每笔交易获得100聪的奖励",
|
|
||||||
"Your referral link": "你的推荐链接",
|
|
||||||
"Your earned rewards": "你获得的奖励",
|
"Your earned rewards": "你获得的奖励",
|
||||||
"Claim": "领取",
|
"Claim": "领取",
|
||||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聪的发票",
|
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聪的发票",
|
||||||
|
@ -40,6 +40,8 @@
|
|||||||
"Learn RoboSats": "學習 RoboSats",
|
"Learn RoboSats": "學習 RoboSats",
|
||||||
"See profile": "查看個人資料",
|
"See profile": "查看個人資料",
|
||||||
"#6": "Phrases in basic/RobotPage/index.tsx",
|
"#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",
|
"Connecting to TOR": "正在連線 TOR",
|
||||||
"Connection encrypted and anonymized using 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.": "這確保最高的隱密程度,但你可能會覺得應用程序運作緩慢。如果丟失連接,請重啟應用。",
|
"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 已開啟",
|
"Telegram enabled": "Telegram 已開啟",
|
||||||
"Enable Telegram Notifications": "開啟 Telegram 通知",
|
"Enable Telegram Notifications": "開啟 Telegram 通知",
|
||||||
"Use stealth invoices": "使用隱形發票",
|
"Use stealth invoices": "使用隱形發票",
|
||||||
"Share to earn 100 Sats per trade": "分享並於每筆交易獲得100聰的獎勵",
|
|
||||||
"Your referral link": "你的推薦鏈接",
|
|
||||||
"Your earned rewards": "你獲得的獎勵",
|
"Your earned rewards": "你獲得的獎勵",
|
||||||
"Claim": "領取",
|
"Claim": "領取",
|
||||||
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聰的發票",
|
"Invoice for {{amountSats}} Sats": "{{amountSats}} 聰的發票",
|
||||||
|
@ -6,7 +6,7 @@ urlpatterns = [
|
|||||||
path("", basic),
|
path("", basic),
|
||||||
path("create/", basic),
|
path("create/", basic),
|
||||||
path("robot/", basic),
|
path("robot/", basic),
|
||||||
path("robot/<refCode>", basic),
|
path("robot/<token>", basic),
|
||||||
path("offers/", basic),
|
path("offers/", basic),
|
||||||
path("order/<int:orderId>", basic),
|
path("order/<int:orderId>", basic),
|
||||||
path("settings/", basic),
|
path("settings/", basic),
|
||||||
|
@ -33,3 +33,4 @@ isort==5.12.0
|
|||||||
flake8==6.0.0
|
flake8==6.0.0
|
||||||
pyflakes==3.0.1
|
pyflakes==3.0.1
|
||||||
django-cors-headers==3.14.0
|
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):
|
class DisableCSRFMiddleware(object):
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
@ -6,3 +29,140 @@ class DisableCSRFMiddleware(object):
|
|||||||
setattr(request, "_dont_enforce_csrf_checks", True)
|
setattr(request, "_dont_enforce_csrf_checks", True)
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
return response
|
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
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
import chat.routing
|
import chat.routing
|
||||||
|
from robosats.middleware import TokenAuthMiddleware
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
|
||||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||||
@ -14,11 +15,13 @@ django_asgi_app = get_asgi_application()
|
|||||||
|
|
||||||
protocols = {}
|
protocols = {}
|
||||||
protocols["websocket"] = AuthMiddlewareStack(
|
protocols["websocket"] = AuthMiddlewareStack(
|
||||||
|
TokenAuthMiddleware(
|
||||||
URLRouter(
|
URLRouter(
|
||||||
chat.routing.websocket_urlpatterns,
|
chat.routing.websocket_urlpatterns,
|
||||||
# add api.routing.websocket_urlpatterns when Order page works with websocket
|
# add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if config("DEVELOPMENT", default=False):
|
if config("DEVELOPMENT", default=False):
|
||||||
protocols["http"] = django_asgi_app
|
protocols["http"] = django_asgi_app
|
||||||
|
@ -11,6 +11,7 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -34,6 +35,9 @@ DEBUG = False
|
|||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATIC_ROOT = "/usr/src/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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
if config("DEVELOPMENT", default=False):
|
if config("DEVELOPMENT", default=False):
|
||||||
@ -92,6 +96,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"corsheaders",
|
"corsheaders",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
|
"rest_framework.authtoken",
|
||||||
"django_celery_beat",
|
"django_celery_beat",
|
||||||
"django_celery_results",
|
"django_celery_results",
|
||||||
"import_export",
|
"import_export",
|
||||||
@ -105,10 +110,13 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||||
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
"TITLE": "RoboSats REST API v0",
|
"TITLE": "RoboSats REST API",
|
||||||
"DESCRIPTION": textwrap.dedent(
|
"DESCRIPTION": textwrap.dedent(
|
||||||
"""
|
"""
|
||||||
REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange
|
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,
|
"SERVE_INCLUDE_SCHEMA": False,
|
||||||
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
||||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||||
@ -145,9 +153,9 @@ MIDDLEWARE = [
|
|||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
# "django.middleware.csrf.CsrfViewMiddleware",
|
|
||||||
"robosats.middleware.DisableCSRFMiddleware",
|
"robosats.middleware.DisableCSRFMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"robosats.middleware.RobotTokenSHA256AuthenticationMiddleWare",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
@ -13,12 +13,21 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
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.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
VERSION = settings.VERSION
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("coordinator/", admin.site.urls),
|
||||||
path("api/", include("api.urls")),
|
path("api/", include("api.urls")),
|
||||||
# path('chat/', include('chat.urls')),
|
# path('chat/', include('chat.urls')),
|
||||||
path("", include("frontend.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