├── .gitignore ├── .vscode └── settings.json ├── README.md ├── core ├── __init__.py ├── admin.py ├── apps.py ├── image_detection.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_membership_payment.py │ ├── 0003_file.py │ ├── 0004_trackedrequest.py │ ├── 0005_membership_stripe_subscription_item_id.py │ ├── 0006_trackedrequest_usage_record_id.py │ ├── 0007_auto_20190522_1332.py │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── haarcascade_frontalface_default.xml ├── home ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ └── prod.py ├── urls.py └── wsgi │ ├── dev.py │ └── prod.py ├── manage.py ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── requirements.txt ├── src ├── App.js ├── App.test.js ├── assets │ └── images │ │ ├── ai.svg │ │ ├── face.png │ │ ├── logo.svg │ │ └── short_paragraph.png ├── constants.js ├── containers │ ├── Account │ │ ├── APIKey.js │ │ ├── Billing.js │ │ ├── ChangeEmail.js │ │ ├── ChangePassword.js │ │ ├── Shell.js │ │ └── SubscribeForm.js │ ├── Demo.js │ ├── LandingPage.js │ ├── Layout.js │ ├── Layout │ │ ├── DesktopContainer.js │ │ └── MobileContainer.js │ ├── Login.js │ └── Signup.js ├── hoc │ └── hoc.js ├── index.js ├── registerServiceWorker.js ├── routes.js ├── store │ ├── actions │ │ ├── actionTypes.js │ │ └── auth.js │ ├── reducers │ │ └── auth.js │ └── utility.js └── utils.js └── thumbnail.png /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | **/*.pyc 3 | **/__pycache__ 4 | node_modules 5 | build 6 | media 7 | static_root -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "${workspaceFolder}/env/bin/python3", 3 | "editor.formatOnSave": true, 4 | "python.linting.pep8Enabled": true, 5 | "python.linting.pylintPath": "pylint", 6 | "python.linting.pylintArgs": ["--load-plugins", "pylint_django"], 7 | "python.linting.pylintEnabled": true, 8 | "python.venvPath": "${workspaceFolder}/env/bin/python3", 9 | "python.linting.pep8Args": ["--ignore=E501"], 10 | "files.exclude": { 11 | "**/*.pyc": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | JustDjango 5 | 6 |

7 |

8 | The Definitive Django Learning Platform. 9 |

10 |

11 | 12 | To learn how to build this application, [head over to our website.](https://justdjango.com) 13 | 14 | This project is a facial recognition API. Users are able to create an account, utilise a free trial of the API, and create a membership to have full access to the API. Payments are handled with Stripe. The API is built with the Django Rest Framework and the frontend is built with React. The image recognition library is OpenCV. The project is deployed on Ubuntu using Digital Ocean. 15 | 16 | ## Backend development workflow 17 | 18 | ```json 19 | virtualenv env 20 | source env/bin/activate 21 | pip install -r requirements.txt 22 | python manage.py runserver 23 | ``` 24 | 25 | ## Frontend development workflow 26 | 27 | ```json 28 | npm i 29 | npm start 30 | ``` 31 | 32 | ## Deployment workflow 33 | 1. Change the manage.py to use the production settings 34 | 2. Change the constants.js file inside src to have DEBUG set to false 35 | 3. Push to the production environment 36 | 4. Run python manage.py migrate 37 | 5. Run npm run build 38 | 6. Run python manage.py collectstatic 39 | 40 | ## Disclaimer 41 | This project does not cover concepts of image recognition - it is a proof of concept teaching how to take an idea from scratch to something real. 42 | 43 | --- 44 | 45 |
46 | 47 | Other places you can find us:
48 | 49 | YouTube 50 | Twitter 51 | 52 |
53 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import User, Membership, Payment, File, TrackedRequest 4 | 5 | admin.site.register(User) 6 | admin.site.register(Membership) 7 | admin.site.register(Payment) 8 | admin.site.register(File) 9 | admin.site.register(TrackedRequest) 10 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /core/image_detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import urllib 3 | import json 4 | import cv2 5 | import os 6 | from django.conf import settings 7 | 8 | execution_path = settings.MEDIA_ROOT 9 | face_detector = os.path.join( 10 | settings.BASE_DIR, "haarcascade_frontalface_default.xml") 11 | 12 | 13 | def detect_faces(image_path=None, url=None): 14 | default = {"safely_executed": False} 15 | if image_path: 16 | true_image_path = os.path.join( 17 | execution_path, image_path.split('/media/')[1]) 18 | image_to_read = read_image(path=true_image_path) 19 | elif url: 20 | image_to_read = read_image(url=url) 21 | else: 22 | default["error_value"] = "There is no image provided" 23 | return default 24 | image_to_read = cv2.cvtColor(image_to_read, cv2.COLOR_BGR2GRAY) 25 | detector_value = cv2.CascadeClassifier(face_detector) 26 | values = detector_value.detectMultiScale(image_to_read, 27 | scaleFactor=1.1, 28 | minNeighbors=5, 29 | minSize=(30, 30), 30 | flags=cv2.CASCADE_SCALE_IMAGE) 31 | values = [(int(a), int(b), int(a + c), int(b + d)) 32 | for (a, b, c, d) in values] 33 | default.update({"number_of_faces": len(values), 34 | "faces": values, 35 | "safely_executed": True}) 36 | return default 37 | 38 | 39 | def read_image(path=None, stream=None, url=None): 40 | if path is not None: 41 | image = cv2.imread(path) 42 | else: 43 | if url is not None: 44 | response = urllib.request.urlopen(url) 45 | data_temp = response.read() 46 | elif stream is not None: 47 | data_temp = stream.read() 48 | image = np.asarray(bytearray(data_temp), dtype="uint8") 49 | image = cv2.imdecode(image, cv2.IMREAD_COLOR) 50 | return image 51 | -------------------------------------------------------------------------------- /core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-21 17:07 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0011_update_proxy_permissions'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('is_member', models.BooleanField(default=False)), 33 | ('on_free_trial', models.BooleanField(default=True)), 34 | ('stripe_customer_id', models.CharField(max_length=40)), 35 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 36 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 37 | ], 38 | options={ 39 | 'verbose_name': 'user', 40 | 'verbose_name_plural': 'users', 41 | 'abstract': False, 42 | }, 43 | managers=[ 44 | ('objects', django.contrib.auth.models.UserManager()), 45 | ], 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /core/migrations/0002_membership_payment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-21 17:12 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Payment', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('timestamp', models.DateTimeField(auto_now_add=True)), 20 | ('amount', models.FloatField()), 21 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Membership', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('type', models.CharField(choices=[('F', 'free_trial'), ('M', 'member'), ('N', 'not_member')], max_length=1)), 29 | ('start_date', models.DateTimeField()), 30 | ('end_date', models.DateTimeField()), 31 | ('stripe_subscription_id', models.CharField(blank=True, max_length=40, null=True)), 32 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /core/migrations/0003_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-21 17:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0002_membership_payment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='File', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('file', models.ImageField(upload_to='')), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /core/migrations/0004_trackedrequest.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-22 08:30 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0003_file'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='TrackedRequest', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('endpoint', models.CharField(max_length=50)), 20 | ('timestamp', models.DateTimeField(auto_now_add=True)), 21 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /core/migrations/0005_membership_stripe_subscription_item_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-22 13:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0004_trackedrequest'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='membership', 15 | name='stripe_subscription_item_id', 16 | field=models.CharField(blank=True, max_length=40, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/0006_trackedrequest_usage_record_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-22 13:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0005_membership_stripe_subscription_item_id'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='trackedrequest', 15 | name='usage_record_id', 16 | field=models.CharField(blank=True, max_length=50, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /core/migrations/0007_auto_20190522_1332.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.1 on 2019-05-22 13:32 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0006_trackedrequest_usage_record_id'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='payment', 17 | name='user', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.signals import user_logged_in 3 | from django.contrib.auth.models import AbstractUser 4 | from django.db.models.signals import post_save 5 | from django.conf import settings 6 | from django.utils import timezone 7 | 8 | import datetime 9 | import stripe 10 | stripe.api_key = settings.STRIPE_SECRET_KEY 11 | 12 | MEMBERSHIP_CHOICES = ( 13 | ('F', 'free_trial'), 14 | ('M', 'member'), 15 | ('N', 'not_member') 16 | ) 17 | 18 | 19 | class File(models.Model): 20 | file = models.ImageField() 21 | 22 | def __str__(self): 23 | return self.file.name 24 | 25 | 26 | class User(AbstractUser): 27 | is_member = models.BooleanField(default=False) 28 | on_free_trial = models.BooleanField(default=True) 29 | stripe_customer_id = models.CharField(max_length=40) 30 | 31 | 32 | class Membership(models.Model): 33 | user = models.OneToOneField(User, on_delete=models.CASCADE) 34 | type = models.CharField(max_length=1, choices=MEMBERSHIP_CHOICES) 35 | start_date = models.DateTimeField() 36 | end_date = models.DateTimeField() 37 | stripe_subscription_id = models.CharField( 38 | max_length=40, blank=True, null=True) 39 | stripe_subscription_item_id = models.CharField( 40 | max_length=40, blank=True, null=True) 41 | 42 | def __str__(self): 43 | return self.user.username 44 | 45 | 46 | class Payment(models.Model): 47 | user = models.ForeignKey(User, on_delete=models.CASCADE) 48 | timestamp = models.DateTimeField(auto_now_add=True) 49 | amount = models.FloatField() 50 | 51 | def __str__(self): 52 | return self.user.username 53 | 54 | 55 | class TrackedRequest(models.Model): 56 | user = models.ForeignKey(User, on_delete=models.CASCADE) 57 | endpoint = models.CharField(max_length=50) 58 | timestamp = models.DateTimeField(auto_now_add=True) 59 | usage_record_id = models.CharField(max_length=50, blank=True, null=True) 60 | 61 | def __str__(self): 62 | return self.user.username 63 | 64 | 65 | def post_save_user_receiver(sender, instance, created, *args, **kwargs): 66 | if created: 67 | customer = stripe.Customer.create(email=instance.email) 68 | instance.stripe_customer_id = customer.id 69 | instance.save() 70 | 71 | membership = Membership.objects.get_or_create( 72 | user=instance, 73 | type='F', 74 | start_date=timezone.now(), 75 | end_date=timezone.now() + datetime.timedelta(days=14) 76 | ) 77 | 78 | 79 | def user_logged_in_receiver(sender, user, request, **kwargs): 80 | membership = user.membership 81 | if user.on_free_trial: 82 | # membership end date has passed 83 | if membership.end_date < timezone.now(): 84 | user.on_free_trial = False 85 | 86 | elif user.is_member: 87 | sub = stripe.Subscription.retrieve(membership.stripe_subscription_id) 88 | if sub.status == "active": 89 | membership.end_date = datetime.datetime.fromtimestamp( 90 | sub.current_period_end 91 | ) 92 | user.is_member = True 93 | else: 94 | user.is_member = False 95 | else: 96 | pass 97 | 98 | user.save() 99 | membership.save() 100 | 101 | 102 | post_save.connect(post_save_user_receiver, sender=User) 103 | user_logged_in.connect(user_logged_in_receiver) 104 | -------------------------------------------------------------------------------- /core/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | from rest_framework.exceptions import PermissionDenied 3 | 4 | 5 | class IsMember(permissions.BasePermission): 6 | 7 | def has_permission(self, request, view): 8 | if request.user.is_authenticated: 9 | if request.user.is_member or request.user.on_free_trial: 10 | return True 11 | else: 12 | raise PermissionDenied( 13 | "You must be a member to make this request") 14 | else: 15 | raise PermissionDenied( 16 | "You must be logged in to make this request") 17 | -------------------------------------------------------------------------------- /core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.authtoken.models import Token 3 | from .models import File 4 | 5 | 6 | class ChangeEmailSerializer(serializers.Serializer): 7 | email = serializers.EmailField() 8 | confirm_email = serializers.EmailField() 9 | 10 | 11 | class ChangePasswordSerializer(serializers.Serializer): 12 | password = serializers.CharField(style={'input_type': 'password'}) 13 | confirm_password = serializers.CharField(style={'input_type': 'password'}) 14 | current_password = serializers.CharField(style={'input_type': 'password'}) 15 | 16 | 17 | class FileSerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = File 20 | fields = "__all__" 21 | 22 | 23 | class TokenSerializer(serializers.ModelSerializer): 24 | class Meta: 25 | model = Token 26 | fields = ('pk', 'key') 27 | 28 | 29 | class SubscribeSerializer(serializers.Serializer): 30 | stripeToken = serializers.CharField(max_length=60) 31 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | FileUploadView, 4 | ChangeEmailView, 5 | UserEmailView, 6 | ChangePasswordView, 7 | UserDetailsView, 8 | SubscribeView, 9 | ImageRecognitionView, 10 | APIKeyView, 11 | CancelSubscription 12 | ) 13 | 14 | app_name = 'core' 15 | 16 | urlpatterns = [ 17 | path('demo/', FileUploadView.as_view(), name='file-upload-demo'), 18 | path('email/', UserEmailView.as_view(), name='email'), 19 | path('change-email/', ChangeEmailView.as_view(), name='change-email'), 20 | path('change-password/', ChangePasswordView.as_view(), name='change-password'), 21 | path('billing/', UserDetailsView.as_view(), name='billing'), 22 | path('subscribe/', SubscribeView.as_view(), name='subscribe'), 23 | path('cancel-subscription/', CancelSubscription.as_view(), 24 | name='cancel-subscription'), 25 | path('upload/', ImageRecognitionView.as_view(), name='image-recognition'), 26 | path('api-key/', APIKeyView.as_view(), name='api-key') 27 | ] 28 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import math 3 | from django.conf import settings 4 | from django.contrib.auth import get_user_model, authenticate 5 | from django.shortcuts import render 6 | from rest_framework.authtoken.models import Token 7 | from rest_framework.permissions import AllowAny, IsAuthenticated 8 | from rest_framework.response import Response 9 | from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST 10 | from rest_framework.views import APIView 11 | from .image_detection import detect_faces 12 | from .models import TrackedRequest, Payment 13 | from .permissions import IsMember 14 | from .serializers import ( 15 | ChangeEmailSerializer, 16 | ChangePasswordSerializer, 17 | FileSerializer, 18 | TokenSerializer, 19 | SubscribeSerializer 20 | ) 21 | 22 | import stripe 23 | stripe.api_key = settings.STRIPE_SECRET_KEY 24 | 25 | User = get_user_model() 26 | 27 | 28 | def get_user_from_token(request): 29 | key = request.META.get("HTTP_AUTHORIZATION").split(' ')[1] 30 | token = Token.objects.get(key=key) 31 | user = User.objects.get(id=token.user_id) 32 | return user 33 | 34 | 35 | class FileUploadView(APIView): 36 | permission_classes = (AllowAny, ) 37 | throttle_scope = 'demo' 38 | 39 | def post(self, request, *args, **kwargs): 40 | 41 | content_length = request.META.get('CONTENT_LENGTH') # bytes 42 | if int(content_length) > 5000000: 43 | return Response({"message": "Image size is greater than 5MB"}, status=HTTP_400_BAD_REQUEST) 44 | 45 | file_serializer = FileSerializer(data=request.data) 46 | if file_serializer.is_valid(): 47 | file_serializer.save() 48 | image_path = file_serializer.data.get('file') 49 | recognition = detect_faces(image_path) 50 | return Response(recognition, status=HTTP_200_OK) 51 | 52 | 53 | class UserEmailView(APIView): 54 | permission_classes = (IsAuthenticated, ) 55 | 56 | def get(self, request, *args, **kwargs): 57 | user = get_user_from_token(request) 58 | obj = {'email': user.email} 59 | return Response(obj) 60 | 61 | 62 | class ChangeEmailView(APIView): 63 | permission_classes = (IsAuthenticated, ) 64 | 65 | def post(self, request, *args, **kwargs): 66 | user = get_user_from_token(request) 67 | email_serializer = ChangeEmailSerializer(data=request.data) 68 | if email_serializer.is_valid(): 69 | print(email_serializer.data) 70 | email = email_serializer.data.get('email') 71 | confirm_email = email_serializer.data.get('confirm_email') 72 | if email == confirm_email: 73 | user.email = email 74 | user.save() 75 | return Response({"email": email}, status=HTTP_200_OK) 76 | return Response({"message": "The emails did not match"}, status=HTTP_400_BAD_REQUEST) 77 | return Response({"message": "Did not receive the correct data"}, status=HTTP_400_BAD_REQUEST) 78 | 79 | 80 | class ChangePasswordView(APIView): 81 | permission_classes = (IsAuthenticated, ) 82 | 83 | def post(self, request, *args, **kwargs): 84 | user = get_user_from_token(request) 85 | password_serializer = ChangePasswordSerializer(data=request.data) 86 | if password_serializer.is_valid(): 87 | password = password_serializer.data.get('password') 88 | confirm_password = password_serializer.data.get('confirm_password') 89 | current_password = password_serializer.data.get('current_password') 90 | auth_user = authenticate( 91 | username=user.username, 92 | password=current_password 93 | ) 94 | if auth_user is not None: 95 | if password == confirm_password: 96 | auth_user.set_password(password) 97 | auth_user.save() 98 | return Response(status=HTTP_200_OK) 99 | else: 100 | return Response({"message": "The passwords did not match"}, status=HTTP_400_BAD_REQUEST) 101 | return Response({"message": "Incorrect user details"}, status=HTTP_400_BAD_REQUEST) 102 | return Response({"message": "Did not receive the correct data"}, status=HTTP_400_BAD_REQUEST) 103 | 104 | 105 | class UserDetailsView(APIView): 106 | permission_classes = (IsAuthenticated, ) 107 | 108 | def get(self, request, *args, **kwargs): 109 | user = get_user_from_token(request) 110 | membership = user.membership 111 | today = datetime.datetime.now() 112 | month_start = datetime.date(today.year, today.month, 1) 113 | tracked_request_count = TrackedRequest.objects \ 114 | .filter(user=user, timestamp__gte=month_start) \ 115 | .count() 116 | amount_due = 0 117 | if user.is_member: 118 | amount_due = stripe.Invoice.upcoming( 119 | customer=user.stripe_customer_id)['amount_due'] / 100 120 | print(amount_due) 121 | obj = { 122 | 'membershipType': membership.get_type_display(), 123 | 'free_trial_end_date': membership.end_date, 124 | 'next_billing_date': membership.end_date, 125 | 'api_request_count': tracked_request_count, 126 | 'amount_due': amount_due 127 | } 128 | return Response(obj) 129 | 130 | 131 | class SubscribeView(APIView): 132 | permission_classes = (IsAuthenticated, ) 133 | 134 | def post(self, request, *args, **kwargs): 135 | user = get_user_from_token(request) 136 | # get the user membership 137 | membership = user.membership 138 | 139 | try: 140 | 141 | # get the stripe customer 142 | customer = stripe.Customer.retrieve(user.stripe_customer_id) 143 | serializer = SubscribeSerializer(data=request.data) 144 | 145 | # serialize post data (stripeToken) 146 | if serializer.is_valid(): 147 | 148 | # get stripeToken from serializer data 149 | stripe_token = serializer.data.get('stripeToken') 150 | 151 | # create the stripe subscription 152 | subscription = stripe.Subscription.create( 153 | customer=customer.id, 154 | items=[{"plan": settings.STRIPE_PLAN_ID}] 155 | ) 156 | 157 | # update the membership 158 | membership.stripe_subscription_id = subscription.id 159 | membership.stripe_subscription_item_id = subscription['items']['data'][0]['id'] 160 | membership.type = 'M' 161 | membership.start_date = datetime.datetime.now() 162 | membership.end_date = datetime.datetime.fromtimestamp( 163 | subscription.current_period_end 164 | ) 165 | membership.save() 166 | 167 | # update the user 168 | user.is_member = True 169 | user.on_free_trial = False 170 | user.save() 171 | 172 | # create the payment 173 | payment = Payment() 174 | payment.amount = subscription.plan.amount / 100 175 | payment.user = user 176 | payment.save() 177 | 178 | return Response({'message': "success"}, status=HTTP_200_OK) 179 | 180 | else: 181 | return Response({'message': "Incorrect data was received"}, status=HTTP_400_BAD_REQUEST) 182 | 183 | except stripe.error.CardError as e: 184 | return Response({'message': "Your card has been declined"}, status=HTTP_400_BAD_REQUEST) 185 | 186 | except stripe.error.StripeError as e: 187 | return Response({'message': "There was an error. You have not been billed. If this persists please contact support"}, status=HTTP_400_BAD_REQUEST) 188 | 189 | except Exception as e: 190 | return Response({"message": "We apologize for the error. We have been informed and are working on the problem."}, status=HTTP_400_BAD_REQUEST) 191 | 192 | 193 | class CancelSubscription(APIView): 194 | permission_classes = (IsMember, ) 195 | 196 | def post(self, request, *args, **kwargs): 197 | user = get_user_from_token(request) 198 | membership = user.membership 199 | 200 | # update the stripe subscription 201 | try: 202 | sub = stripe.Subscription.retrieve( 203 | membership.stripe_subscription_id) 204 | sub.delete() 205 | except Exception as e: 206 | return Response({"message": "We apologize for the error. We have been informed and are working on the problem."}, status=HTTP_400_BAD_REQUEST) 207 | 208 | # update the user 209 | user.is_member = False 210 | user.save() 211 | 212 | # update the membership 213 | membership.type = "N" 214 | membership.save() 215 | 216 | return Response({'message': "Your subscription has been cancelled."}, status=HTTP_200_OK) 217 | 218 | 219 | class ImageRecognitionView(APIView): 220 | permission_classes = (IsMember, ) 221 | 222 | def post(self, request, *args, **kwargs): 223 | user = get_user_from_token(request) 224 | membership = user.membership 225 | file_serializer = FileSerializer(data=request.data) 226 | 227 | usage_record_id = None 228 | if user.is_member and not user.on_free_trial: 229 | usage_record = stripe.UsageRecord.create( 230 | quantity=1, 231 | timestamp=math.floor(datetime.datetime.now().timestamp()), 232 | subscription_item=membership.stripe_subscription_item_id 233 | ) 234 | usage_record_id = usage_record.id 235 | 236 | tracked_request = TrackedRequest() 237 | tracked_request.user = user 238 | tracked_request.usage_record_id = usage_record_id 239 | tracked_request.endpoint = '/api/image-recognition/' 240 | tracked_request.save() 241 | 242 | content_length = request.META.get('CONTENT_LENGTH') # bytes 243 | if int(content_length) > 5000000: 244 | return Response({"message": "Image size is greater than 5MB"}, status=HTTP_400_BAD_REQUEST) 245 | 246 | if file_serializer.is_valid(): 247 | file_serializer.save() 248 | image_path = file_serializer.data.get('file') 249 | recognition = detect_faces(image_path) 250 | return Response(recognition, status=HTTP_200_OK) 251 | return Response({"Received incorrect data"}, status=HTTP_400_BAD_REQUEST) 252 | 253 | 254 | class APIKeyView(APIView): 255 | permission_classes = (IsAuthenticated, ) 256 | 257 | def get(self, request, *args, **kwargs): 258 | user = get_user_from_token(request) 259 | token_qs = Token.objects.filter(user=user) 260 | if token_qs.exists(): 261 | token_serializer = TokenSerializer(token_qs, many=True) 262 | try: 263 | return Response(token_serializer.data, status=HTTP_200_OK) 264 | except: 265 | return Response({"message": "Did not receive correct data"}, status=HTTP_400_BAD_REQUEST) 266 | return Response({"message": "User does not exist"}, status=HTTP_400_BAD_REQUEST) 267 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/db.sqlite3 -------------------------------------------------------------------------------- /home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/home/__init__.py -------------------------------------------------------------------------------- /home/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/home/settings/__init__.py -------------------------------------------------------------------------------- /home/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname( 4 | os.path.dirname(os.path.abspath(__file__)))) 5 | SECRET_KEY = '-05sgp9!deq=q1nltm@^^2cc+v29i(tyybv3v2t77qi66czazj' 6 | DEBUG = True 7 | ALLOWED_HOSTS = [] 8 | 9 | INSTALLED_APPS = [ 10 | 'django.contrib.admin', 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.messages', 15 | 'django.contrib.staticfiles', 16 | 17 | 'django.contrib.sites', 18 | 'allauth', 19 | 'allauth.account', 20 | 'allauth.socialaccount', 21 | 'corsheaders', 22 | 'rest_auth', 23 | 'rest_auth.registration', 24 | 'rest_framework', 25 | 'rest_framework.authtoken', 26 | 27 | 'core' 28 | ] 29 | 30 | MIDDLEWARE = [ 31 | 'corsheaders.middleware.CorsMiddleware', 32 | 'django.middleware.security.SecurityMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.common.CommonMiddleware', 35 | 'django.middleware.csrf.CsrfViewMiddleware', 36 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 37 | 'django.contrib.messages.middleware.MessageMiddleware', 38 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 39 | ] 40 | 41 | ROOT_URLCONF = 'home.urls' 42 | 43 | TEMPLATES = [ 44 | { 45 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 46 | 'DIRS': [os.path.join(BASE_DIR, 'build')], 47 | 'APP_DIRS': True, 48 | 'OPTIONS': { 49 | 'context_processors': [ 50 | 'django.template.context_processors.debug', 51 | 'django.template.context_processors.request', 52 | 'django.contrib.auth.context_processors.auth', 53 | 'django.contrib.messages.context_processors.messages', 54 | ], 55 | }, 56 | }, 57 | ] 58 | 59 | LANGUAGE_CODE = 'en-us' 60 | TIME_ZONE = 'UTC' 61 | USE_I18N = True 62 | USE_L10N = True 63 | USE_TZ = True 64 | 65 | STATIC_URL = '/static/' 66 | MEDIA_URL = '/media/' 67 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'build/static')] 68 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 69 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 70 | SITE_ID = 1 71 | 72 | REST_FRAMEWORK = { 73 | 'DEFAULT_PERMISSION_CLASSES': ( 74 | 'rest_framework.permissions.IsAuthenticated', 75 | 'rest_framework.permissions.AllowAny', 76 | ), 77 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 78 | 'rest_framework.authentication.TokenAuthentication', 79 | ), 80 | 'DEFAULT_THROTTLE_CLASSES': ( 81 | 'rest_framework.throttling.ScopedRateThrottle', 82 | 'rest_framework.throttling.AnonRateThrottle', 83 | 'rest_framework.throttling.UserRateThrottle' 84 | ), 85 | 'DEFAULT_THROTTLE_RATES': { 86 | 'demo': '3/day', 87 | 'anon': '24/day', 88 | 'user': '1000/day' 89 | } 90 | } 91 | 92 | CSRF_COOKIE_NAME = "csrftoken" 93 | 94 | AUTH_USER_MODEL = 'core.User' 95 | ACCOUNT_EMAIL_REQUIRED = False 96 | ACCOUNT_AUTHENTICATION_METHOD = 'username' 97 | ACCOUNT_EMAIL_VERIFICATION = 'none' 98 | 99 | # Stripe plan ID 100 | 101 | STRIPE_PLAN_ID = 'facial_monthly_api_call' 102 | -------------------------------------------------------------------------------- /home/settings/dev.py: -------------------------------------------------------------------------------- 1 | '''Use this for development''' 2 | 3 | from .base import * 4 | 5 | ALLOWED_HOSTS += ['127.0.0.1'] 6 | DEBUG = True 7 | 8 | WSGI_APPLICATION = 'home.wsgi.dev.application' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 14 | } 15 | } 16 | 17 | CORS_ORIGIN_WHITELIST = ( 18 | 'localhost:3000', 19 | ) 20 | 21 | STRIPE_PUBLISH_KEY = '' 22 | STRIPE_SECRET_KEY = '' 23 | -------------------------------------------------------------------------------- /home/settings/prod.py: -------------------------------------------------------------------------------- 1 | '''Use this for production''' 2 | 3 | from .base import * 4 | 5 | DEBUG = False 6 | ALLOWED_HOSTS += [''] 7 | WSGI_APPLICATION = 'home.wsgi.prod.application' 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 12 | 'NAME': '', 13 | 'USER': '', 14 | 'PASSWORD': '', 15 | 'HOST': 'localhost', 16 | 'PORT': '', 17 | } 18 | } 19 | 20 | AUTH_PASSWORD_VALIDATORS = [ 21 | {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, 22 | {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, 23 | {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, 24 | {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, 25 | ] 26 | 27 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 28 | 29 | STRIPE_PUBLISH_KEY = '' 30 | STRIPE_SECRET_KEY = '' 31 | 32 | CORS_ORIGIN_ALLOW_ALL = True 33 | -------------------------------------------------------------------------------- /home/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include, re_path 3 | from django.views.generic import TemplateView 4 | 5 | urlpatterns = [ 6 | path('api-auth/', include('rest_framework.urls')), 7 | path('rest-auth/', include('rest_auth.urls')), 8 | path('rest-auth/registration/', include('rest_auth.registration.urls')), 9 | path('admin/', admin.site.urls), 10 | path('api/', include('core.urls', namespace='api')), 11 | re_path(r'^.*', TemplateView.as_view(template_name='index.html')), 12 | ] 13 | -------------------------------------------------------------------------------- /home/wsgi/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.dev") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /home/wsgi/prod.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | from whitenoise.django import DjangoWhiteNoise 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.prod") 7 | 8 | application = get_wsgi_application() 9 | application = DjangoWhiteNoise(application) 10 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "home.settings.dev") 6 | try: 7 | from django.core.management import execute_from_command_line 8 | except ImportError as exc: 9 | raise ImportError( 10 | "Couldn't import Django. Are you sure it's installed and " 11 | "available on your PYTHONPATH environment variable? Did you " 12 | "forget to activate a virtual environment?" 13 | ) from exc 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-react-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.19.0", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.4.1", 9 | "react-redux": "^5.1.1", 10 | "react-router-dom": "^4.3.1", 11 | "react-scripts": "1.1.4", 12 | "react-stripe-elements": "^3.0.0", 13 | "redux": "^4.0.1", 14 | "redux-thunk": "^2.3.0", 15 | "semantic-ui-css": "^2.4.1", 16 | "semantic-ui-react": "^0.87.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "engines": { 25 | "node": "8.9.1", 26 | "npm": "5.5.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Facial recognition SAAS 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.4.4 2 | certifi==2018.8.24 3 | chardet==3.0.4 4 | defusedxml==0.5.0 5 | Django==2.2.13 6 | django-allauth==0.37.1 7 | django-cors-headers==2.4.0 8 | django-rest-auth==0.9.3 9 | djangorestframework==3.9.4 10 | gunicorn==19.7.1 11 | idna==2.7 12 | numpy==1.16.3 13 | oauthlib==2.1.0 14 | opencv-python==4.1.0.25 15 | pep8==1.7.1 16 | Pillow==6.0.0 17 | psycopg2-binary==2.8.2 18 | pycodestyle==2.5.0 19 | python3-openid==3.1.0 20 | pytz==2018.5 21 | requests==2.20.0 22 | requests-oauthlib==1.0.0 23 | six==1.11.0 24 | sqlparse==0.3.0 25 | stripe==2.28.1 26 | urllib3==1.24.3 27 | whitenoise==3.3.1 28 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import BaseRouter from "./routes"; 5 | import "semantic-ui-css/semantic.min.css"; 6 | import * as actions from "./store/actions/auth"; 7 | 8 | import CustomLayout from "./containers/Layout"; 9 | 10 | class App extends Component { 11 | componentDidMount() { 12 | this.props.onTryAutoSignup(); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | const mapStateToProps = state => { 27 | return { 28 | isAuthenticated: state.auth.token !== null 29 | }; 30 | }; 31 | 32 | const mapDispatchToProps = dispatch => { 33 | return { 34 | onTryAutoSignup: () => dispatch(actions.authCheckState()) 35 | }; 36 | }; 37 | 38 | export default connect( 39 | mapStateToProps, 40 | mapDispatchToProps 41 | )(App); 42 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/assets/images/ai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/src/assets/images/face.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/short_paragraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/src/assets/images/short_paragraph.png -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | let DEBUG = false; 2 | let host = "http://127.0.0.1:8000"; 3 | let stripePublishKey = ""; 4 | if (DEBUG === false) { 5 | host = ""; 6 | stripePublishKey = ""; 7 | } 8 | 9 | export { stripePublishKey }; 10 | 11 | export const APIEndpoint = `${host}/api`; 12 | 13 | export const fileUploadURL = `${APIEndpoint}/demo/`; 14 | export const facialRecognitionURL = `${APIEndpoint}/upload/`; 15 | export const emailURL = `${APIEndpoint}/email/`; 16 | export const changeEmailURL = `${APIEndpoint}/change-email/`; 17 | export const changePasswordURL = `${APIEndpoint}/change-password/`; 18 | export const billingURL = `${APIEndpoint}/billing/`; 19 | export const subscribeURL = `${APIEndpoint}/subscribe/`; 20 | export const APIkeyURL = `${APIEndpoint}/api-key/`; 21 | export const cancelSubscriptionURL = `${APIEndpoint}/cancel-subscription/`; 22 | 23 | export const loginURL = `${host}/rest-auth/login/`; 24 | export const signupURL = `${host}/rest-auth/registration/`; 25 | -------------------------------------------------------------------------------- /src/containers/Account/APIKey.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Segment, 4 | Header, 5 | Icon, 6 | Dimmer, 7 | Loader, 8 | Image, 9 | Button, 10 | Message 11 | } from "semantic-ui-react"; 12 | import Shell from "./Shell"; 13 | import ShortParagraphIMG from "../../assets/images/short_paragraph.png"; 14 | import { authAxios } from "../../utils"; 15 | import { APIkeyURL } from "../../constants"; 16 | 17 | class APIKey extends React.Component { 18 | state = { 19 | loading: false, 20 | error: null, 21 | keys: [] 22 | }; 23 | 24 | componentDidMount() { 25 | this.handleUserDetails(); 26 | } 27 | 28 | handleUserDetails = () => { 29 | this.setState({ loading: true }); 30 | authAxios 31 | .get(APIkeyURL) 32 | .then(res => { 33 | this.setState({ 34 | loading: false, 35 | keys: res.data 36 | }); 37 | }) 38 | .catch(err => { 39 | this.setState({ 40 | loading: false, 41 | error: err.response.data.message 42 | }); 43 | }); 44 | }; 45 | 46 | render() { 47 | const { loading, error, keys } = this.state; 48 | return ( 49 | 50 | {error && } 51 | {loading && ( 52 | 53 | 54 | Detecting faces... 55 | 56 | 57 | 58 | )} 59 | {keys && ( 60 | 61 |
API Key
62 | {keys.map(k => { 63 | return

{k.key}

; 64 | })} 65 |
66 | )} 67 |
68 | ); 69 | } 70 | } 71 | 72 | export default APIKey; 73 | -------------------------------------------------------------------------------- /src/containers/Account/Billing.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Segment, 4 | Header, 5 | Icon, 6 | Dimmer, 7 | Loader, 8 | Image, 9 | Button, 10 | Modal, 11 | Divider 12 | } from "semantic-ui-react"; 13 | import Shell from "./Shell"; 14 | import SubscribeForm from "./SubscribeForm"; 15 | import ShortParagraphIMG from "../../assets/images/short_paragraph.png"; 16 | import { authAxios } from "../../utils"; 17 | import { billingURL, cancelSubscriptionURL } from "../../constants"; 18 | 19 | class Billing extends React.Component { 20 | state = { 21 | error: null, 22 | loading: false, 23 | billingDetails: {}, 24 | open: false 25 | }; 26 | 27 | componentDidMount() { 28 | this.handleUserDetails(); 29 | } 30 | 31 | show = size => () => this.setState({ size, open: true }); 32 | 33 | close = () => this.setState({ open: false }); 34 | 35 | handleUnsubscribe = () => { 36 | this.setState({ 37 | error: null, 38 | loading: true 39 | }); 40 | authAxios 41 | .post(cancelSubscriptionURL) 42 | .then(res => { 43 | this.setState({ 44 | loading: false 45 | }); 46 | this.close(); 47 | this.handleUserDetails(); 48 | }) 49 | .catch(err => { 50 | this.setState({ 51 | error: err.response.data.message, 52 | loading: false 53 | }); 54 | }); 55 | }; 56 | 57 | handleUserDetails = () => { 58 | this.setState({ 59 | loading: true 60 | }); 61 | authAxios 62 | .get(billingURL) 63 | .then(res => { 64 | this.setState({ 65 | loading: false, 66 | billingDetails: res.data 67 | }); 68 | }) 69 | .catch(err => { 70 | this.setState({ 71 | loading: false, 72 | error: err.response.data.message 73 | }); 74 | }); 75 | }; 76 | 77 | renderBillingDetails(details) { 78 | const free_trial = "free_trial"; 79 | const member = "member"; 80 | const not_member = "not_member"; 81 | return ( 82 | 83 |
Monthly summary
84 | {details.membershipType === free_trial ? ( 85 | 86 |

87 | Your free trial ends on{" "} 88 | {new Date(details.free_trial_end_date).toDateString()} 89 |

90 |

API requests this month: {details.api_request_count}

91 | 92 |
93 | ) : details.membershipType === member ? ( 94 | 95 |

Next billing date: {details.next_billing_date}

96 |

API requests this month: {details.api_request_count}

97 |

Amount due: ${details.amount_due}

98 | 99 | 100 |
101 | ) : details.membershipType === not_member ? ( 102 | 103 |

Your free trial has ended

104 | 105 |
106 | ) : null} 107 |
108 | ); 109 | } 110 | 111 | render() { 112 | const { loading, error, billingDetails, open, size } = this.state; 113 | return ( 114 | 115 | 116 | {error && ( 117 | 118 |
119 | 120 | Could not fetch your account details. Try reloading the page 121 |
122 | 123 | 124 | 125 |
126 | )} 127 | {loading && ( 128 | 129 | 130 | Detecting faces... 131 | 132 | 133 | 134 | )} 135 | {billingDetails && this.renderBillingDetails(billingDetails)} 136 |
137 | 138 | 139 | Cancel Your Subscription 140 | 141 |

Are you sure you want to cancel your subscription?

142 |
143 | 144 | 147 | 121 | 122 | 123 | ); 124 | } 125 | } 126 | export default ChangeEmail; 127 | -------------------------------------------------------------------------------- /src/containers/Account/ChangePassword.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Input, Message, Header, Button } from "semantic-ui-react"; 3 | import Shell from "./Shell"; 4 | import { authAxios } from "../../utils"; 5 | import { changePasswordURL } from "../../constants"; 6 | 7 | class ChangePassword extends React.Component { 8 | state = { 9 | currentPassword: "", 10 | password: "", 11 | confirmPassword: "", 12 | error: null, 13 | loading: false 14 | }; 15 | 16 | handleChange = e => { 17 | this.setState({ 18 | [e.target.name]: e.target.value, 19 | error: null 20 | }); 21 | }; 22 | 23 | handleSubmit = e => { 24 | e.preventDefault(); 25 | this.setState({ 26 | loading: true 27 | }); 28 | const { password, confirmPassword, currentPassword } = this.state; 29 | if (password === "" || confirmPassword === "") { 30 | this.setState({ 31 | error: "Please fill in all the fields", 32 | loading: false 33 | }); 34 | } else { 35 | if (password === confirmPassword) { 36 | authAxios 37 | .post(changePasswordURL, { 38 | current_password: currentPassword, 39 | password, 40 | confirm_password: confirmPassword 41 | }) 42 | .then(res => { 43 | this.setState({ 44 | loading: false, 45 | password: "", 46 | confirmPassword: "", 47 | currentPassword: "" 48 | }); 49 | }) 50 | .catch(err => { 51 | this.setState({ 52 | loading: false, 53 | error: err.response.data.message 54 | }); 55 | }); 56 | } else { 57 | this.setState({ 58 | loading: false, 59 | error: "Your passwords do not match" 60 | }); 61 | } 62 | } 63 | }; 64 | 65 | render() { 66 | const { 67 | currentPassword, 68 | password, 69 | confirmPassword, 70 | error, 71 | loading 72 | } = this.state; 73 | return ( 74 | 75 |
Change password
76 |
77 | 78 | 79 | 86 | 87 | 88 | 89 | 96 | 97 | 98 | 99 | 106 | 107 | {error && } 108 | 111 | 112 |
113 | ); 114 | } 115 | } 116 | export default ChangePassword; 117 | -------------------------------------------------------------------------------- /src/containers/Account/Shell.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid, Container, Segment, Header, Menu } from "semantic-ui-react"; 3 | import { Link } from "react-router-dom"; 4 | import { connect } from "react-redux"; 5 | import { withRouter } from "react-router-dom"; 6 | 7 | import { logout } from "../../store/actions/auth"; 8 | 9 | const Shell = props => ( 10 | 11 | 12 | 13 | 14 | 15 |
Account
16 | 17 | props.history.push("/account/change-email")} 20 | name="change-email" 21 | > 22 | Change Email 23 | 24 | props.history.push("/account/change-password")} 27 | name="change-email" 28 | > 29 | Change Password 30 | 31 | props.history.push("/account/billing")} 34 | name="change-email" 35 | > 36 | Billing 37 | 38 | props.history.push("/account/api-key")} 41 | name="change-email" 42 | > 43 | API Key 44 | 45 | props.logout()} name="change-email"> 46 | Logout 47 | 48 | 49 |
50 | {props.children} 51 |
52 |
53 |
54 |
55 | ); 56 | 57 | const mapDispatchToProps = dispatch => { 58 | return { 59 | logout: () => dispatch(logout()) 60 | }; 61 | }; 62 | 63 | export default withRouter( 64 | connect( 65 | null, 66 | mapDispatchToProps 67 | )(Shell) 68 | ); 69 | -------------------------------------------------------------------------------- /src/containers/Account/SubscribeForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | CardElement, 4 | injectStripe, 5 | Elements, 6 | StripeProvider 7 | } from "react-stripe-elements"; 8 | import { Divider, Button, Message } from "semantic-ui-react"; 9 | import { authAxios } from "../../utils"; 10 | import { subscribeURL, stripePublishKey } from "../../constants"; 11 | 12 | class StripeForm extends React.Component { 13 | state = { 14 | loading: false, 15 | error: null 16 | }; 17 | 18 | submit = ev => { 19 | ev.preventDefault(); 20 | this.setState({ loading: true, error: null }); 21 | if (this.props.stripe) { 22 | this.props.stripe.createToken().then(result => { 23 | if (result.error) { 24 | this.setState({ 25 | error: result.error.message, 26 | loading: false 27 | }); 28 | } else { 29 | authAxios 30 | .post(subscribeURL, { 31 | stripeToken: result.token.id 32 | }) 33 | .then(res => { 34 | this.setState({ 35 | loading: false 36 | }); 37 | this.props.handleUserDetails(); 38 | }) 39 | .catch(err => { 40 | console.log(err); 41 | this.setState({ 42 | loading: false, 43 | error: err.response.data.message 44 | }); 45 | }); 46 | } 47 | }); 48 | } else { 49 | console.log("Stripe js hasn't loaded yet"); 50 | } 51 | }; 52 | 53 | render() { 54 | const { loading, error } = this.state; 55 | return ( 56 | 57 | 58 | {error && } 59 | 60 | 69 | 70 | ); 71 | } 72 | } 73 | 74 | const WrappedStripeForm = injectStripe(StripeForm); 75 | 76 | class SubscribeForm extends React.Component { 77 | render() { 78 | return ( 79 | 80 |
81 | 82 | 83 | 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | export default SubscribeForm; 91 | -------------------------------------------------------------------------------- /src/containers/Demo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Container, 5 | Form, 6 | Icon, 7 | Segment, 8 | Divider, 9 | Message, 10 | Progress, 11 | Loader, 12 | Image, 13 | Dimmer 14 | } from "semantic-ui-react"; 15 | import axios from "axios"; 16 | import { authAxios } from "../utils"; 17 | import { fileUploadURL, facialRecognitionURL } from "../constants"; 18 | import FaceIMG from "../assets/images/face.png"; 19 | import ShortParagraphIMG from "../assets/images/short_paragraph.png"; 20 | 21 | class Demo extends React.Component { 22 | state = { 23 | fileName: "", 24 | file: null, 25 | error: null, 26 | loading: false, 27 | statusCode: null, 28 | progress: 0, 29 | spinner: false, 30 | data: null 31 | }; 32 | 33 | handleFileChange = e => { 34 | if (e.target.files[0]) { 35 | const size = e.target.files[0].size; 36 | if (size > 5000000) { 37 | this.setState({ error: "Image size is greater than 5MB" }); 38 | } else { 39 | this.setState({ 40 | fileName: e.target.files[0].name, 41 | file: e.target.files[0], 42 | error: null 43 | }); 44 | } 45 | } 46 | }; 47 | 48 | handleSubmit = e => { 49 | e.preventDefault(); 50 | if (this.state.file) { 51 | this.handleFileUpload(this.state.file); 52 | } else { 53 | this.setState({ 54 | error: "No file selected" 55 | }); 56 | } 57 | }; 58 | 59 | handleFileUpload = async file => { 60 | const __demo_class = this; 61 | const formData = new FormData(); 62 | formData.append("file", file); 63 | this.setState({ loading: true }); 64 | const config = { 65 | onUploadProgress: function(progressEvent) { 66 | const progress = Math.round( 67 | (100 * progressEvent.loaded) / progressEvent.total 68 | ); 69 | __demo_class.setState({ progress }); 70 | if (progress === 100) { 71 | __demo_class.setState({ 72 | loading: false, 73 | spinner: true 74 | }); 75 | } 76 | } 77 | }; 78 | axios 79 | // authAxios 80 | .post(fileUploadURL, formData, config) // facialRecognitionURL 81 | .then(res => { 82 | this.setState({ 83 | data: res.data, 84 | statusCode: res.status, 85 | spinner: false 86 | }); 87 | }) 88 | .catch(err => { 89 | this.setState({ 90 | error: err.message, 91 | loading: false, 92 | spinner: false 93 | }); 94 | }); 95 | }; 96 | 97 | render() { 98 | const { error, progress, loading, spinner, data } = this.state; 99 | return ( 100 | 101 | 102 | Try uploading an image 103 |
104 | 105 | 106 | 112 | 118 | 125 | 128 | 129 |