diff --git a/api/admin.py b/api/admin.py index dd761dd2..15bebbaf 100644 --- a/api/admin.py +++ b/api/admin.py @@ -24,8 +24,8 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(admin.ModelAdmin): - list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at') - list_display_links = ('maker','taker') + list_display = ('id','type','maker','taker','status','amount','currency','created_at','expires_at', 'invoice') + list_display_links = ['id'] pass @admin.register(Profile) diff --git a/api/models.py b/api/models.py index 150c4c35..10131d1d 100644 --- a/api/models.py +++ b/api/models.py @@ -44,11 +44,11 @@ class Order(models.Model): UPI = 15, 'Updated invoice' DIS = 16, 'In dispute' MLD = 17, 'Maker lost dispute' - TLD = 18, 'Taker lost dispute' - EXP = 19, 'Expired' + # TLD = 18, 'Taker lost dispute' + # EXP = 19, 'Expired' - # order info, id = models.CharField(max_length=64, unique=True, null=False) - status = models.PositiveSmallIntegerField(choices=Status.choices, default=Status.WFB) + # order info + status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=int(Status.WFB)) created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() @@ -79,6 +79,7 @@ class Order(models.Model): invoice = models.CharField(max_length=300, unique=False, null=True, default=None) class Profile(models.Model): + user = models.OneToOneField(User,on_delete=models.CASCADE) # Ratings stored as a comma separated integer list @@ -91,7 +92,7 @@ class Profile(models.Model): lost_disputes = models.PositiveIntegerField(null=False, default=0) # RoboHash - avatar = models.ImageField(default="static/assets/avatars/unknown.png", verbose_name='Avatar') + avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar') @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): @@ -102,13 +103,18 @@ class Profile(models.Model): def save_user_profile(sender, instance, **kwargs): instance.profile.save() + @receiver(pre_delete, sender=User) + def del_avatar_from_disk(sender, instance, **kwargs): + avatar_file=Path('frontend/' + instance.profile.avatar.url) + avatar_file.unlink() # FIX deleting user fails if avatar is not found + def __str__(self): return self.user.username # to display avatars in admin panel def get_avatar(self): if not self.avatar: - return 'static/assets/avatars/unknown.png' + return 'static/assets/misc/unknown_avatar.png' return self.avatar.url # method to create a fake table field in read only mode diff --git a/api/serializers.py b/api/serializers.py index b73cb157..722a7f4a 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from .models import Order -class OrderSerializer(serializers.ModelSerializer): +class ListOrderSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker') @@ -9,4 +9,9 @@ class OrderSerializer(serializers.ModelSerializer): class MakeOrderSerializer(serializers.ModelSerializer): class Meta: model = Order - fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') \ No newline at end of file + fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') + +class UpdateOrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = ('id','invoice') \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index 1af71120..eae708dd 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,9 +1,9 @@ from django.urls import path -from .views import MakeOrder, OrderView, UserGenerator, BookView +from .views import OrderMakerView, OrderView, UserView, BookView urlpatterns = [ - path('make/', MakeOrder.as_view()), - path('order/', OrderView.as_view()), - path('usergen/', UserGenerator.as_view()), + path('make/', OrderMakerView.as_view()), + path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})), + path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 5eaf1a95..33555171 100644 --- a/api/views.py +++ b/api/views.py @@ -1,11 +1,13 @@ -from rest_framework import serializers, status +from rest_framework import status +from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView +from rest_framework import viewsets from rest_framework.response import Response + from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User -from django.conf.urls.static import static -from .serializers import OrderSerializer, MakeOrderSerializer +from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .models import Order from .nick_generator.nick_generator import NickGenerator @@ -24,9 +26,27 @@ expiration_time = 8 avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) +def validate_already_maker_or_taker(request): + '''Checks if the user is already partipant of an order''' + + queryset = Order.objects.filter(maker=request.user.id) + if queryset.exists(): + return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST) + + queryset = Order.objects.filter(taker=request.user.id) + if queryset.exists(): + return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST) + + return True, None + +def validate_ln_invoice(invoice): + '''Checks if a LN invoice is valid''' + #TODO + return True + # Create your views here. -class MakeOrder(APIView): +class OrderMakerView(CreateAPIView): serializer_class = MakeOrderSerializer def post(self,request): @@ -41,17 +61,14 @@ class MakeOrder(APIView): satoshis = serializer.data.get('satoshis') is_explicit = serializer.data.get('is_explicit') - # query if the user is already a maker or taker, return error - queryset = Order.objects.filter(maker=request.user.id) - if queryset.exists(): - return Response({'Bad Request':'You are already maker of an order'},status=status.HTTP_400_BAD_REQUEST) - queryset = Order.objects.filter(taker=request.user.id) - if queryset.exists(): - return Response({'Bad Request':'You are already taker of an order'},status=status.HTTP_400_BAD_REQUEST) + valid, response = validate_already_maker_or_taker(request) + if not valid: + return response # Creates a new order in db order = Order( type=otype, + status=int(Order.Status.PUB), # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) currency=currency, amount=amount, payment_method=payment_method, @@ -65,11 +82,11 @@ class MakeOrder(APIView): if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) - return Response(OrderSerializer(order).data, status=status.HTTP_201_CREATED) + return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) -class OrderView(APIView): - serializer_class = OrderSerializer +class OrderView(viewsets.ViewSet): + serializer_class = UpdateOrderSerializer lookup_url_kwarg = 'order_id' def get(self, request, format=None): @@ -81,15 +98,14 @@ class OrderView(APIView): # check if exactly one order is found in the db if len(order) == 1 : order = order[0] - data = self.serializer_class(order).data + data = ListOrderSerializer(order).data nickname = request.user.username - # Check if requester is participant in the order and add boolean to response - data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname) - #To do fix: data['status_message'] = Order.Status.get(order.status).label data['status_message'] = Order.Status.WFB.label # Hardcoded WFB, should use order.status value. - + + # Check if requester is participant in the order and add boolean to response + data['is_participant'] = (str(order.maker) == nickname or str(order.taker) == nickname) data['maker_nick'] = str(order.maker) data['taker_nick'] = str(order.taker) @@ -105,7 +121,48 @@ class OrderView(APIView): return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) -class UserGenerator(APIView): + def take_or_update(self, request, format=None): + order_id = request.GET.get(self.lookup_url_kwarg) + + serializer = UpdateOrderSerializer(data=request.data) + order = Order.objects.get(id=order_id) + + if serializer.is_valid(): + invoice = serializer.data.get('invoice') + + # If this is an empty POST request (no invoice), it must be taker request! + if not invoice and order.status == int(Order.Status.PUB): + + valid, response = validate_already_maker_or_taker(request) + if not valid: + return response + + order.taker = self.request.user + order.status = int(Order.Status.TAK) + data = ListOrderSerializer(order).data + + # An invoice came in! update it + elif invoice: + if validate_ln_invoice(invoice): + order.invoice = invoice + + #TODO Validate if request comes from PARTICIPANT AND BUYER + + #If the order status was Payment Failed. Move foward to invoice Updated. + if order.status == int(Order.Status.FAI): + order.status = int(Order.Status.UPI) + + else: + return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) + + # Something else is going on. Probably not allowed. + else: + return Response({'bad_request':'Not allowed'}) + + order.save() + return self.get(request) + +class UserView(APIView): lookup_url_kwarg = 'token' NickGen = NickGenerator( lang='English', @@ -114,6 +171,7 @@ class UserGenerator(APIView): use_noun=True, max_num=999) + # Probably should be turned into a post method def get(self,request, format=None): ''' Get a new user derived from a high entropy token @@ -181,40 +239,39 @@ class UserGenerator(APIView): def delete(self,request): user = User.objects.get(id = request.user.id) - # TO DO. Pressing give me another will delete the logged in user + # TO DO. Pressing "give me another" deletes the logged in user # However it might be a long time recovered user # Only delete if user live is < 5 minutes # TODO check if user exists AND it is not a maker or taker! if user is not None: - avatar_file = avatar_path.joinpath(str(request.user)+".png") - avatar_file.unlink() # Unsafe if avatar does not exist. logout(request) user.delete() - return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_301_MOVED_PERMANENTLY) + return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND) return Response(status=status.HTTP_403_FORBIDDEN) -class BookView(APIView): - serializer_class = OrderSerializer +class BookView(ListAPIView): + serializer_class = ListOrderSerializer def get(self,request, format=None): currency = request.GET.get('currency') type = request.GET.get('type') - queryset = Order.objects.filter(currency=currency, type=type, status=0) # TODO status = 1 for orders that are Public + queryset = Order.objects.filter(currency=currency, type=type, status=int(Order.Status.PUB)) if len(queryset)== 0: return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND) queryset = queryset.order_by('created_at') book_data = [] for order in queryset: - data = OrderSerializer(order).data + data = ListOrderSerializer(order).data user = User.objects.filter(id=data['maker']) if len(user) == 1: data['maker_nick'] = user[0].username - # TODO avoid sending status and takers for book views - #data.pop('status','taker') + # Non participants should not see the status or who is the taker + for key in ('status','taker'): + del data[key] book_data.append(data) return Response(book_data, status=status.HTTP_200_OK) diff --git a/dev_utils/reinitiate_db.sh b/dev_utils/reinitiate_db.sh new file mode 100644 index 00000000..32d63e95 --- /dev/null +++ b/dev_utils/reinitiate_db.sh @@ -0,0 +1,15 @@ +#!/bin/bash +rm db.sqlite3 + +rm -R api/migrations +rm -R frontend/migrations +rm -R frontend/static/assets/avatars + +python3 manage.py makemigrations +python3 manage.py makemigrations api + +python3 manage.py migrate + +python3 manage.py createsuperuser + +python3 manage.py runserver \ No newline at end of file diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index 61d38a46..fa344cbf 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -91,7 +91,7 @@ export default class MakerPage extends Component { console.log(this.state) const requestOptions = { method: 'POST', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, body: JSON.stringify({ type: this.state.type, currency: this.state.currency, diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 2e6db655..b3df2543 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -2,6 +2,23 @@ import React, { Component } from "react"; import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core" import { Link } from 'react-router-dom' +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} +const csrftoken = getCookie('csrftoken'); + // pretty numbers function pn(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); @@ -26,7 +43,7 @@ export default class OrderPage extends Component { statusText: data.status_message, type: data.type, currency: data.currency, - currencyCode: (data.currency== 1 ) ? "USD": ((data.currency == 2 ) ? "EUR":"ETH"), + currencyCode: this.getCurrencyCode(data.currency), amount: data.amount, paymentMethod: data.payment_method, isExplicit: data.is_explicit, @@ -41,10 +58,29 @@ export default class OrderPage extends Component { }); } + // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) + // Improve this function so currencies are read from json + getCurrencyCode(val){ + return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH") + } + // Fix to use proper react props handleClickBackButton=()=>{ window.history.back(); } + + handleClickTakeOrderButton=()=>{ + console.log(this.state) + const requestOptions = { + method: 'POST', + headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + body: JSON.stringify({}), + }; + fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) + .then((response) => response.json()) + .then((data) => (console.log(data) & this.getOrderDetails(data.id))); + } + render (){ return ( @@ -53,7 +89,7 @@ export default class OrderPage extends Component { BTC {this.state.type ? " Sell " : " Buy "} Order - + + + + : ""} @@ -98,14 +140,16 @@ export default class OrderPage extends Component { + + - {this.state.isParticipant ? "" : } + {this.state.isParticipant ? "" : } - + ); diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index bdab24d0..0332043b 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -74,7 +74,7 @@ export default class UserGenPage extends Component { this.setState({ token: this.genBase62Token(32), }) - this.getGeneratedUser(); + this.reload_for_csrf_to_work(); } handleChangeToken=(e)=>{ diff --git a/frontend/static/assets/misc/unknown_avatar.png b/frontend/static/assets/misc/unknown_avatar.png new file mode 100644 index 00000000..9e19b6a9 Binary files /dev/null and b/frontend/static/assets/misc/unknown_avatar.png differ