mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 02:21:35 +00:00
Delete user view, session auth and png avatars (#588)
This commit is contained in:
parent
13a1fac202
commit
ca3f7cb222
@ -65,7 +65,7 @@ class Robot(models.Model):
|
|||||||
|
|
||||||
# RoboHash
|
# RoboHash
|
||||||
avatar = models.ImageField(
|
avatar = models.ImageField(
|
||||||
default=("static/assets/avatars/" + "unknown_avatar.png"),
|
default=("static/assets/avatars/" + "unknown_avatar.webp"),
|
||||||
verbose_name="Avatar",
|
verbose_name="Avatar",
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
@ -356,241 +356,6 @@ class OrderViewSchema:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserViewSchema:
|
|
||||||
post = {
|
|
||||||
"summary": "Create user",
|
|
||||||
"description": textwrap.dedent(
|
|
||||||
"""
|
|
||||||
Create a new Robot 🤖
|
|
||||||
|
|
||||||
`token_sha256` is the SHA256 hash of your token. 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 recieves the hash of the
|
|
||||||
token, it trusts the client with computing `length`, `counts` and `unique_values`
|
|
||||||
correctly. Check [here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.js#L13)
|
|
||||||
to see how the Javascript client copmutes these values. These values are optional,
|
|
||||||
but if provided, the api computes the entropy of the token adds two additional
|
|
||||||
fields to the response JSON - `token_shannon_entropy` and `token_bits_entropy`.
|
|
||||||
|
|
||||||
**Note: It is entirely the clients responsibilty to generate high entropy tokens, and the optional
|
|
||||||
parameters are provided to act as an aid to help determine sufficient entropy, but the server is happy
|
|
||||||
with just any sha256 hash you provide it**
|
|
||||||
|
|
||||||
`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": {
|
|
||||||
201: {
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
"token_bits_entropy": {"type": "integer"},
|
|
||||||
"token_shannon_entropy": {"type": "integer"},
|
|
||||||
"wants_stealth": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": False,
|
|
||||||
"description": "Whether the user prefers stealth invoices",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
202: {
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
"token_bits_entropy": {"type": "integer"},
|
|
||||||
"token_shannon_entropy": {"type": "integer"},
|
|
||||||
"wants_stealth": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": False,
|
|
||||||
"description": "Whether the user prefers stealth invoices",
|
|
||||||
},
|
|
||||||
"found": {"type": "string", "description": "Welcome back message"},
|
|
||||||
"tg_enabled": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "The robot has telegram notifications enabled",
|
|
||||||
},
|
|
||||||
"tg_token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Token to enable telegram with /start <tg_token>",
|
|
||||||
},
|
|
||||||
"tg_bot_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of the coordinator's telegram bot",
|
|
||||||
},
|
|
||||||
"last_login": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"description": "Last time seen",
|
|
||||||
},
|
|
||||||
"active_order_id": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Active order id if present",
|
|
||||||
},
|
|
||||||
"last_order_id": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Last order id if present",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
400: {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"active_order_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Order id the robot is a maker/taker of",
|
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Username (Robot name)",
|
|
||||||
},
|
|
||||||
"bad_request": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Reason for the failure",
|
|
||||||
"default": "You are already logged in as {nickname} and have an active order",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"description": "Response when you already authenticated and have an order",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"bad_request": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Reason for the failure",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
403: {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"bad_request": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Reason for the failure",
|
|
||||||
"default": "Enter a different token",
|
|
||||||
},
|
|
||||||
"found": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Bad luck, this nickname is taken",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"examples": [
|
|
||||||
OpenApiExample(
|
|
||||||
"Successfuly created user",
|
|
||||||
value={
|
|
||||||
"token_shannon_entropy": 0.7714559798089662,
|
|
||||||
"token_bits_entropy": 169.21582985307933,
|
|
||||||
"nickname": "StackerMan420",
|
|
||||||
"public_key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n......\n......",
|
|
||||||
"encrypted_private_key": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n......\n......",
|
|
||||||
"wants_stealth": False,
|
|
||||||
},
|
|
||||||
status_codes=[201],
|
|
||||||
),
|
|
||||||
OpenApiExample(
|
|
||||||
"Already authenticated and have an order",
|
|
||||||
value={
|
|
||||||
"active_order_id": "42069",
|
|
||||||
"nickname": "StackerMan210",
|
|
||||||
"bad_request": "You are already logged in as {nickname} and have an active order",
|
|
||||||
},
|
|
||||||
status_codes=[400],
|
|
||||||
),
|
|
||||||
OpenApiExample(
|
|
||||||
"When required token entropy not met",
|
|
||||||
value={"bad_request": "The token does not have enough entropy"},
|
|
||||||
status_codes=[400],
|
|
||||||
),
|
|
||||||
OpenApiExample(
|
|
||||||
"Invalid PGP public key provided",
|
|
||||||
value={"bad_request": "Your PGP public key does not seem valid"},
|
|
||||||
status_codes=[400],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
delete = {
|
|
||||||
"summary": "Delete user",
|
|
||||||
"description": textwrap.dedent(
|
|
||||||
"""
|
|
||||||
Delete a Robot. Deleting a robot is not allowed if the robot has an active order, has had completed trades or was created more than 30 mins ago.
|
|
||||||
Mainly used on the frontend to "Generate new Robot" without flooding the DB with discarded robots.
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
"responses": {
|
|
||||||
403: {},
|
|
||||||
400: {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"bad_request": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Reason for the failure",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
301: {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"user_deleted": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "User deleted permanently",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BookViewSchema:
|
class BookViewSchema:
|
||||||
get = {
|
get = {
|
||||||
"summary": "Get public orders",
|
"summary": "Get public orders",
|
||||||
@ -627,8 +392,6 @@ class RobotViewSchema:
|
|||||||
"summary": "Get robot info",
|
"summary": "Get robot info",
|
||||||
"description": textwrap.dedent(
|
"description": textwrap.dedent(
|
||||||
"""
|
"""
|
||||||
DEPRECATED: Use `/robot` GET.
|
|
||||||
|
|
||||||
Get robot info 🤖
|
Get robot info 🤖
|
||||||
|
|
||||||
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
||||||
|
@ -527,64 +527,6 @@ class UpdateOrderSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserGenSerializer(serializers.Serializer):
|
|
||||||
# Mandatory fields
|
|
||||||
token_sha256 = serializers.CharField(
|
|
||||||
min_length=64,
|
|
||||||
max_length=64,
|
|
||||||
allow_null=False,
|
|
||||||
allow_blank=False,
|
|
||||||
required=True,
|
|
||||||
help_text="SHA256 of user secret",
|
|
||||||
)
|
|
||||||
# Optional fields
|
|
||||||
# (PGP keys are mandatory for new users, but optional for logins)
|
|
||||||
public_key = serializers.CharField(
|
|
||||||
max_length=2000,
|
|
||||||
allow_null=False,
|
|
||||||
allow_blank=False,
|
|
||||||
required=False,
|
|
||||||
help_text="Armored ASCII PGP public key block",
|
|
||||||
)
|
|
||||||
encrypted_private_key = serializers.CharField(
|
|
||||||
max_length=2000,
|
|
||||||
allow_null=False,
|
|
||||||
allow_blank=False,
|
|
||||||
required=False,
|
|
||||||
help_text="Armored ASCII PGP encrypted private key block",
|
|
||||||
)
|
|
||||||
|
|
||||||
ref_code = serializers.CharField(
|
|
||||||
max_length=30,
|
|
||||||
allow_null=True,
|
|
||||||
allow_blank=True,
|
|
||||||
required=False,
|
|
||||||
default=None,
|
|
||||||
help_text="Referal code",
|
|
||||||
)
|
|
||||||
counts = serializers.ListField(
|
|
||||||
child=serializers.IntegerField(),
|
|
||||||
allow_null=True,
|
|
||||||
required=False,
|
|
||||||
default=None,
|
|
||||||
help_text="Counts of the unique characters in the token",
|
|
||||||
)
|
|
||||||
length = serializers.IntegerField(
|
|
||||||
allow_null=True,
|
|
||||||
default=None,
|
|
||||||
required=False,
|
|
||||||
min_value=1,
|
|
||||||
help_text="Length of the token",
|
|
||||||
)
|
|
||||||
unique_values = serializers.IntegerField(
|
|
||||||
allow_null=True,
|
|
||||||
default=None,
|
|
||||||
required=False,
|
|
||||||
min_value=1,
|
|
||||||
help_text="Number of unique values in the token",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ClaimRewardSerializer(serializers.Serializer):
|
class ClaimRewardSerializer(serializers.Serializer):
|
||||||
invoice = serializers.CharField(
|
invoice = serializers.CharField(
|
||||||
max_length=2000,
|
max_length=2000,
|
||||||
|
225
api/views.py
225
api/views.py
@ -1,26 +1,18 @@
|
|||||||
import hashlib
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from math import log2
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate, login, logout
|
|
||||||
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
|
||||||
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.authentication import TokenAuthentication
|
||||||
from rest_framework.generics import CreateAPIView, ListAPIView
|
from rest_framework.generics import CreateAPIView, ListAPIView
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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 scipy.stats import entropy
|
|
||||||
|
|
||||||
from api.logics import Logics
|
from api.logics import Logics
|
||||||
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
|
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
|
||||||
@ -37,7 +29,6 @@ from api.oas_schemas import (
|
|||||||
RobotViewSchema,
|
RobotViewSchema,
|
||||||
StealthViewSchema,
|
StealthViewSchema,
|
||||||
TickViewSchema,
|
TickViewSchema,
|
||||||
UserViewSchema,
|
|
||||||
)
|
)
|
||||||
from api.serializers import (
|
from api.serializers import (
|
||||||
ClaimRewardSerializer,
|
ClaimRewardSerializer,
|
||||||
@ -49,7 +40,6 @@ from api.serializers import (
|
|||||||
StealthSerializer,
|
StealthSerializer,
|
||||||
TickSerializer,
|
TickSerializer,
|
||||||
UpdateOrderSerializer,
|
UpdateOrderSerializer,
|
||||||
UserGenSerializer,
|
|
||||||
)
|
)
|
||||||
from api.utils import (
|
from api.utils import (
|
||||||
compute_avg_premium,
|
compute_avg_premium,
|
||||||
@ -57,14 +47,11 @@ from api.utils import (
|
|||||||
get_cln_version,
|
get_cln_version,
|
||||||
get_lnd_version,
|
get_lnd_version,
|
||||||
get_robosats_commit,
|
get_robosats_commit,
|
||||||
validate_pgp_keys,
|
|
||||||
verify_signed_message,
|
verify_signed_message,
|
||||||
)
|
)
|
||||||
from chat.models import Message
|
from chat.models import Message
|
||||||
from control.models import AccountingDay, BalanceLog
|
from control.models import AccountingDay, BalanceLog
|
||||||
|
|
||||||
from .nick_generator.nick_generator import NickGenerator
|
|
||||||
|
|
||||||
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
|
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
|
||||||
RETRY_TIME = int(config("RETRY_TIME"))
|
RETRY_TIME = int(config("RETRY_TIME"))
|
||||||
PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1
|
PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1
|
||||||
@ -79,7 +66,7 @@ avatar_path.mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
class MakerView(CreateAPIView):
|
class MakerView(CreateAPIView):
|
||||||
serializer_class = MakeOrderSerializer
|
serializer_class = MakeOrderSerializer
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(**MakerViewSchema.post)
|
@extend_schema(**MakerViewSchema.post)
|
||||||
@ -190,7 +177,7 @@ class MakerView(CreateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class OrderView(viewsets.ViewSet):
|
class OrderView(viewsets.ViewSet):
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
serializer_class = UpdateOrderSerializer
|
serializer_class = UpdateOrderSerializer
|
||||||
lookup_url_kwarg = "order_id"
|
lookup_url_kwarg = "order_id"
|
||||||
@ -652,7 +639,7 @@ class OrderView(viewsets.ViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class RobotView(APIView):
|
class RobotView(APIView):
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(**RobotViewSchema.get)
|
@extend_schema(**RobotViewSchema.get)
|
||||||
@ -692,208 +679,6 @@ class RobotView(APIView):
|
|||||||
return Response(context, status=status.HTTP_200_OK)
|
return Response(context, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class UserView(APIView):
|
|
||||||
"""
|
|
||||||
Deprecated. UserView will be completely replaced by the smaller RobotView in
|
|
||||||
combination with the RobotTokenSHA256 middleware (on-the-fly robot generation)
|
|
||||||
"""
|
|
||||||
|
|
||||||
NickGen = NickGenerator(
|
|
||||||
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer_class = UserGenSerializer
|
|
||||||
|
|
||||||
@extend_schema(**UserViewSchema.post)
|
|
||||||
def post(self, request, format=None):
|
|
||||||
"""
|
|
||||||
Get a new user derived from a high entropy token
|
|
||||||
|
|
||||||
- Request has a hash of a high-entropy token
|
|
||||||
- Request includes pubKey and encrypted privKey
|
|
||||||
- Generates new nickname and avatar.
|
|
||||||
- Creates login credentials (new User object)
|
|
||||||
|
|
||||||
Response with Avatar, Nickname, pubKey, privKey.
|
|
||||||
"""
|
|
||||||
context = {}
|
|
||||||
serializer = self.serializer_class(data=request.data)
|
|
||||||
|
|
||||||
# Return bad request if serializer is not valid
|
|
||||||
if not serializer.is_valid():
|
|
||||||
context = {"bad_request": "Invalid serializer"}
|
|
||||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# The new way. The token is never sent. Only its SHA256
|
|
||||||
token_sha256 = serializer.data.get("token_sha256")
|
|
||||||
public_key = serializer.data.get("public_key")
|
|
||||||
encrypted_private_key = serializer.data.get("encrypted_private_key")
|
|
||||||
|
|
||||||
# Now the server only receives a hash of the token. So server trusts the client
|
|
||||||
# with computing length, counts and unique_values to confirm the high entropy of the token
|
|
||||||
# In any case, it is up to the client if they want to create a bad high entropy token.
|
|
||||||
|
|
||||||
# Submitting the three params needed to compute token entropy is not mandatory
|
|
||||||
# If not submitted, avatars can be created with garbage entropy token. Frontend will always submit them.
|
|
||||||
try:
|
|
||||||
unique_values = serializer.data.get("unique_values")
|
|
||||||
counts = serializer.data.get("counts")
|
|
||||||
length = serializer.data.get("length")
|
|
||||||
|
|
||||||
shannon_entropy = entropy(counts, base=62)
|
|
||||||
bits_entropy = log2(unique_values**length)
|
|
||||||
|
|
||||||
# Payload
|
|
||||||
context = {
|
|
||||||
"token_shannon_entropy": shannon_entropy,
|
|
||||||
"token_bits_entropy": bits_entropy,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
|
|
||||||
if bits_entropy < 128 or shannon_entropy < 0.7:
|
|
||||||
context["bad_request"] = "The token does not have enough entropy"
|
|
||||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token, aka RoboSats ID)
|
|
||||||
hash = hashlib.sha256(token_sha256.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
# Generate nickname deterministically
|
|
||||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
|
||||||
context["nickname"] = nickname
|
|
||||||
|
|
||||||
# Generate avatar
|
|
||||||
rh = Robohash(hash)
|
|
||||||
rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
|
|
||||||
|
|
||||||
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
|
|
||||||
# Deprecate "png" and keep "webp" only after v0.4.4
|
|
||||||
image_path = avatar_path.joinpath(nickname + ".webp")
|
|
||||||
if not image_path.exists():
|
|
||||||
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)
|
|
||||||
|
|
||||||
png_path = avatar_path.joinpath(nickname + ".png")
|
|
||||||
with open(png_path, "wb") as f:
|
|
||||||
rh.img.save(f, format="png", optimize=True)
|
|
||||||
|
|
||||||
# Create new credentials and login if nickname is new
|
|
||||||
if len(User.objects.filter(username=nickname)) == 0:
|
|
||||||
if not public_key or not encrypted_private_key:
|
|
||||||
context[
|
|
||||||
"bad_request"
|
|
||||||
] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
|
|
||||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
|
||||||
(
|
|
||||||
valid,
|
|
||||||
bad_keys_context,
|
|
||||||
public_key,
|
|
||||||
encrypted_private_key,
|
|
||||||
) = validate_pgp_keys(public_key, encrypted_private_key)
|
|
||||||
if not valid:
|
|
||||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
User.objects.create_user(
|
|
||||||
username=nickname, password=token_sha256, is_staff=False
|
|
||||||
)
|
|
||||||
user = authenticate(request, username=nickname, password=token_sha256)
|
|
||||||
login(request, user)
|
|
||||||
|
|
||||||
user.robot.avatar = "static/assets/avatars/" + nickname + ".webp"
|
|
||||||
|
|
||||||
# Noticed some PGP keys replaced at re-login. Should not happen.
|
|
||||||
# Let's implement this sanity check "If robot has not keys..."
|
|
||||||
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
|
|
||||||
|
|
||||||
user.robot.save()
|
|
||||||
|
|
||||||
context = {**context, **Telegram.get_context(user)}
|
|
||||||
context["public_key"] = user.robot.public_key
|
|
||||||
context["encrypted_private_key"] = user.robot.encrypted_private_key
|
|
||||||
context["wants_stealth"] = user.robot.wants_stealth
|
|
||||||
return Response(context, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
# log in user and return pub/priv keys if existing
|
|
||||||
else:
|
|
||||||
user = authenticate(request, username=nickname, password=token_sha256)
|
|
||||||
if user is not None:
|
|
||||||
login(request, user)
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Sends the welcome back message.
|
|
||||||
context["found"] = "We found your Robot avatar. Welcome back!"
|
|
||||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
|
||||||
else:
|
|
||||||
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion chance)
|
|
||||||
context["found"] = "Bad luck, this nickname is taken"
|
|
||||||
context["bad_request"] = "Enter a different token"
|
|
||||||
return Response(context, status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
@extend_schema(**UserViewSchema.delete)
|
|
||||||
def delete(self, request):
|
|
||||||
"""Pressing "give me another" deletes the logged in user"""
|
|
||||||
user = request.user
|
|
||||||
if not user.is_authenticated:
|
|
||||||
return Response(status.HTTP_403_FORBIDDEN)
|
|
||||||
|
|
||||||
# Only delete if user life is shorter than 30 minutes. Helps to avoid deleting users by mistake
|
|
||||||
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
|
|
||||||
return Response(status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
# Check if it is not a maker or taker!
|
|
||||||
not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
|
|
||||||
if not not_participant:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"bad_request": "Maybe a mistake? User cannot be deleted while he is part of an order"
|
|
||||||
},
|
|
||||||
status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Check if has already a robot with
|
|
||||||
if user.robot.total_contracts > 0:
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades"
|
|
||||||
},
|
|
||||||
status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
logout(request)
|
|
||||||
user.delete()
|
|
||||||
return Response(
|
|
||||||
{"user_deleted": "User deleted permanently"},
|
|
||||||
status.HTTP_301_MOVED_PERMANENTLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BookView(ListAPIView):
|
class BookView(ListAPIView):
|
||||||
serializer_class = OrderPublicSerializer
|
serializer_class = OrderPublicSerializer
|
||||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||||
@ -1016,7 +801,7 @@ class InfoView(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class RewardView(CreateAPIView):
|
class RewardView(CreateAPIView):
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
serializer_class = ClaimRewardSerializer
|
serializer_class = ClaimRewardSerializer
|
||||||
@ -1156,7 +941,7 @@ class HistoricalView(ListAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class StealthView(APIView):
|
class StealthView(APIView):
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
serializer_class = StealthSerializer
|
serializer_class = StealthSerializer
|
||||||
|
@ -5,9 +5,6 @@ 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.authentication import TokenAuthentication
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,7 +17,7 @@ from chat.serializers import ChatSerializer, PostMessageSerializer
|
|||||||
|
|
||||||
class ChatView(viewsets.ViewSet):
|
class ChatView(viewsets.ViewSet):
|
||||||
serializer_class = PostMessageSerializer
|
serializer_class = PostMessageSerializer
|
||||||
authentication_classes = [TokenAuthentication, SessionAuthentication]
|
authentication_classes = [TokenAuthentication]
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
lookup_url_kwarg = ["order_id", "offset"]
|
lookup_url_kwarg = ["order_id", "offset"]
|
||||||
|
@ -18,7 +18,6 @@ python-decouple==3.8
|
|||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
ring==0.10.1
|
ring==0.10.1
|
||||||
git+https://github.com/RoboSats/Robohash.git
|
git+https://github.com/RoboSats/Robohash.git
|
||||||
scipy==1.10.1
|
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
psycopg2==2.9.7
|
psycopg2==2.9.7
|
||||||
SQLAlchemy==2.0.16
|
SQLAlchemy==2.0.16
|
||||||
|
@ -5,7 +5,6 @@ from channels.db import database_sync_to_async
|
|||||||
from channels.middleware import BaseMiddleware
|
from channels.middleware import BaseMiddleware
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser, User, update_last_login
|
from django.contrib.auth.models import AnonymousUser, User, update_last_login
|
||||||
from django.db import IntegrityError
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from robohash import Robohash
|
from robohash import Robohash
|
||||||
@ -97,15 +96,7 @@ class RobotTokenSHA256AuthenticationMiddleWare:
|
|||||||
# Generate nickname deterministically
|
# Generate nickname deterministically
|
||||||
nickname = NickGen.short_from_SHA256(hash, max_length=18)[0]
|
nickname = NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||||
|
|
||||||
# DEPRECATE. Using Try and Except only as a temporary measure.
|
user = User.objects.create_user(username=nickname, password=None)
|
||||||
# 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.
|
# Django rest_framework authtokens are limited to 40 characters.
|
||||||
# We use base91 so we can store the full entropy in the field.
|
# We use base91 so we can store the full entropy in the field.
|
||||||
|
Loading…
Reference in New Issue
Block a user