├── .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 |
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 |
50 |
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 |
14 | You need to enable JavaScript to run this app.
15 |
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 |
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 |
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 | Cancel subscription
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 | Reload
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 |
145 | No
146 |
147 |
154 |
155 |
156 |
157 | );
158 | }
159 | }
160 |
161 | export default Billing;
162 |
--------------------------------------------------------------------------------
/src/containers/Account/ChangeEmail.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 { changeEmailURL, emailURL } from "../../constants";
6 |
7 | class ChangeEmail extends React.Component {
8 | state = {
9 | currentEmail: "",
10 | email: "",
11 | confirmEmail: "",
12 | error: null,
13 | loading: false
14 | };
15 |
16 | componentDidMount() {
17 | this.handleUserDetails();
18 | }
19 |
20 | handleUserDetails = () => {
21 | this.setState({
22 | loading: true
23 | });
24 | authAxios
25 | .get(emailURL)
26 | .then(res => {
27 | this.setState({
28 | loading: false,
29 | currentEmail: res.data.email
30 | });
31 | })
32 | .catch(err => {
33 | this.setState({
34 | loading: false,
35 | error: err.response.data.message
36 | });
37 | });
38 | };
39 |
40 | handleChange = e => {
41 | this.setState({
42 | [e.target.name]: e.target.value,
43 | error: null
44 | });
45 | };
46 |
47 | handleSubmit = e => {
48 | e.preventDefault();
49 | this.setState({
50 | loading: true
51 | });
52 | const { email, confirmEmail } = this.state;
53 | if (email === "" || confirmEmail === "") {
54 | this.setState({
55 | error: "Please fill in all the fields"
56 | });
57 | } else {
58 | if (email === confirmEmail) {
59 | authAxios
60 | .post(changeEmailURL, {
61 | email,
62 | confirm_email: confirmEmail
63 | })
64 | .then(res => {
65 | this.setState({
66 | loading: false,
67 | email: "",
68 | confirmEmail: "",
69 | currentEmail: email
70 | });
71 | })
72 | .catch(err => {
73 | this.setState({
74 | loading: false,
75 | error: err.response.data.message
76 | });
77 | });
78 | } else {
79 | this.setState({
80 | loading: false,
81 | error: "Your emails do not match"
82 | });
83 | }
84 | }
85 | };
86 |
87 | render() {
88 | const { currentEmail, email, confirmEmail, error, loading } = this.state;
89 | return (
90 |
91 |
92 |
94 | Current email
95 |
96 |
97 |
98 | New email
99 |
106 |
107 |
108 | Confirm email
109 |
116 |
117 | {error && }
118 |
119 | Submit
120 |
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 |
76 |
78 | Current password
79 |
86 |
87 |
88 | New password
89 |
96 |
97 |
98 | Confirm password
99 |
106 |
107 | {error && }
108 |
109 | Submit
110 |
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 |
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 |
67 | Go pro
68 |
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 |
105 | File input & upload
106 |
107 |
108 |
109 |
110 | Choose a File (Max 2MB)
111 |
112 |
118 |
125 |
126 | Upload
127 |
128 |
129 |
130 | Download test image
131 |
132 |
133 |
134 | {error && (
135 |
136 | )}
137 |
138 |
139 | Endpoint
140 |
141 | POST to {facialRecognitionURL} with headers: "Authentication":
142 | "Token {""}"
143 |
144 |
145 |
146 | JSON Response
147 | {loading && (
148 |
154 | File upload progress
155 |
156 | )}
157 |
158 | {spinner && (
159 |
160 |
161 | Detecting faces...
162 |
163 |
164 |
165 | )}
166 | {data && (
167 |
168 |
{JSON.stringify(data, null, 2)}
169 |
170 | )}
171 |
172 | );
173 | }
174 | }
175 |
176 | export default Demo;
177 |
--------------------------------------------------------------------------------
/src/containers/LandingPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Button,
4 | Container,
5 | Grid,
6 | Header,
7 | Image,
8 | List,
9 | Segment
10 | } from "semantic-ui-react";
11 | import { Link } from "react-router-dom";
12 | import AI from "../assets/images/ai.svg";
13 |
14 | const HomepageLayout = () => (
15 |
16 |
17 |
18 |
19 |
20 |
21 | Facial recognition API
22 |
23 |
24 | Try out our cheap and fast facial recognition API. No credit card
25 | required
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Get started
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 | Test our API for free for the new two weeks. No credit card required.
50 |
51 |
52 |
53 | Start my free trial
54 |
55 |
56 |
59 |
60 | Pay for what you use, at $0.05 per request.
61 |
62 |
63 |
64 | Start now
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Sitemap
77 | Contact Us
78 |
79 |
80 |
81 |
82 |
83 | FAQ
84 | Pricing
85 | API
86 |
87 |
88 |
89 |
90 | Facial recognition SAAS
91 |
92 | Find faces in your images
93 |
94 |
95 |
96 |
97 |
98 |
99 | );
100 |
101 | export default HomepageLayout;
102 |
--------------------------------------------------------------------------------
/src/containers/Layout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { withRouter } from "react-router-dom";
4 | import DesktopContainer from "./Layout/DesktopContainer";
5 | import MobileContainer from "./Layout/MobileContainer";
6 |
7 | const ResponsiveContainer = ({ children }) => (
8 |
9 | {children}
10 | {children}
11 |
12 | );
13 |
14 | ResponsiveContainer.propTypes = {
15 | children: PropTypes.node
16 | };
17 |
18 | class CustomLayout extends React.Component {
19 | render() {
20 | return {this.props.children} ;
21 | }
22 | }
23 |
24 | export default withRouter(CustomLayout);
25 |
--------------------------------------------------------------------------------
/src/containers/Layout/DesktopContainer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { Component } from "react";
3 | import {
4 | Button,
5 | Container,
6 | Menu,
7 | Responsive,
8 | Segment,
9 | Visibility
10 | } from "semantic-ui-react";
11 | import { connect } from "react-redux";
12 | import { logout } from "../../store/actions/auth";
13 | import { withRouter } from "react-router-dom";
14 | import { getWidth } from "../../utils";
15 |
16 | class DesktopContainer extends Component {
17 | state = {};
18 |
19 | hideFixedMenu = () => this.setState({ fixed: false });
20 | showFixedMenu = () => this.setState({ fixed: true });
21 |
22 | render() {
23 | const { children, isAuthenticated } = this.props;
24 | const { fixed } = this.state;
25 | return (
26 |
27 |
32 |
38 |
45 |
46 | this.props.history.push("/")}
49 | >
50 | Home
51 |
52 | this.props.history.push("/demo")}
55 | >
56 | Demo
57 |
58 |
59 | {isAuthenticated ? (
60 |
61 | this.props.logout()}
64 | >
65 | Logout
66 |
67 |
71 | this.props.history.push("/account/change-email")
72 | }
73 | >
74 | Account
75 |
76 |
77 | ) : (
78 |
79 | this.props.history.push("/login")}
82 | >
83 | Login
84 |
85 | this.props.history.push("/signup")}
90 | >
91 | Signup
92 |
93 |
94 | )}
95 |
96 |
97 |
98 |
99 |
100 |
101 | {children}
102 |
103 | );
104 | }
105 | }
106 |
107 | DesktopContainer.propTypes = {
108 | children: PropTypes.node
109 | };
110 |
111 | const mapStateToProps = state => {
112 | return {
113 | isAuthenticated: state.auth.token !== null
114 | };
115 | };
116 |
117 | const mapDispatchToProps = dispatch => {
118 | return {
119 | logout: () => dispatch(logout())
120 | };
121 | };
122 |
123 | export default withRouter(
124 | connect(
125 | mapStateToProps,
126 | mapDispatchToProps
127 | )(DesktopContainer)
128 | );
129 |
--------------------------------------------------------------------------------
/src/containers/Layout/MobileContainer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import React, { Component } from "react";
3 | import {
4 | Button,
5 | Container,
6 | Icon,
7 | Menu,
8 | Responsive,
9 | Segment,
10 | Sidebar
11 | } from "semantic-ui-react";
12 | import { withRouter } from "react-router-dom";
13 | import { connect } from "react-redux";
14 | import { logout } from "../../store/actions/auth";
15 | import { getWidth } from "../../utils";
16 |
17 | class MobileContainer extends Component {
18 | state = {};
19 |
20 | handleSidebarHide = () => this.setState({ sidebarOpened: false });
21 |
22 | handleToggle = () => this.setState({ sidebarOpened: true });
23 |
24 | render() {
25 | const { children, isAuthenticated } = this.props;
26 | const { sidebarOpened } = this.state;
27 | return (
28 |
33 |
41 | this.props.history.push("/")}
44 | >
45 | Home
46 |
47 | this.props.history.push("/demo")}
50 | >
51 | Demo
52 |
53 |
54 | {isAuthenticated ? (
55 |
56 | this.props.logout()}>
57 | Logout
58 |
59 |
63 | this.props.history.push("/account/change-email")
64 | }
65 | >
66 | Account
67 |
68 |
69 | ) : (
70 |
71 | this.props.history.push("/login")}
74 | >
75 | Login
76 |
77 | this.props.history.push("/signup")}
81 | >
82 | Signup
83 |
84 |
85 | )}
86 |
87 |
88 |
89 |
90 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | {isAuthenticated ? (
103 | this.props.logout()}>
104 | Logout
105 |
106 | ) : (
107 |
108 | this.props.history.push("/login")}
111 | >
112 | Login
113 |
114 | this.props.history.push("/signup")}
118 | >
119 | Signup
120 |
121 |
122 | )}
123 |
124 |
125 |
126 |
127 |
128 | {children}
129 |
130 |
131 | );
132 | }
133 | }
134 |
135 | MobileContainer.propTypes = {
136 | children: PropTypes.node
137 | };
138 |
139 | const mapStateToProps = state => {
140 | return {
141 | isAuthenticated: state.auth.token !== null
142 | };
143 | };
144 |
145 | const mapDispatchToProps = dispatch => {
146 | return {
147 | logout: () => dispatch(logout())
148 | };
149 | };
150 |
151 | export default withRouter(
152 | connect(
153 | mapStateToProps,
154 | mapDispatchToProps
155 | )(MobileContainer)
156 | );
157 |
--------------------------------------------------------------------------------
/src/containers/Login.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Button,
4 | Form,
5 | Grid,
6 | Header,
7 | Image,
8 | Message,
9 | Segment
10 | } from "semantic-ui-react";
11 | import { connect } from "react-redux";
12 | import { Link, Redirect } from "react-router-dom";
13 | import Logo from "../assets/images/logo.svg";
14 | import { authLogin as login } from "../store/actions/auth";
15 |
16 | class NormalLoginForm extends React.Component {
17 | state = {
18 | username: "",
19 | password: "",
20 | formError: null
21 | };
22 |
23 | handleSubmit = e => {
24 | e.preventDefault();
25 | const { username, password } = this.state;
26 | if (username !== "" && password !== "") {
27 | this.props.login(username, password);
28 | } else {
29 | this.setState({
30 | formError: "Please enter all the form fields"
31 | });
32 | }
33 | };
34 |
35 | handleChange = e => {
36 | this.setState({
37 | [e.target.name]: e.target.value,
38 | formError: null
39 | });
40 | };
41 |
42 | render() {
43 | const { formError } = this.state;
44 | const { loading, error, authenticated } = this.props;
45 | if (authenticated) {
46 | return ;
47 | }
48 | return (
49 |
50 |
55 |
56 |
57 | Login to your account
58 |
59 |
90 | {formError && (
91 |
92 | There was an error
93 | {formError}
94 |
95 | )}
96 | {error && (
97 |
98 | There was an error
99 | {error}
100 |
101 | )}
102 |
103 | New to us? Sign Up
104 |
105 |
106 |
107 |
108 | );
109 | }
110 | }
111 |
112 | const mapStateToProps = state => {
113 | return {
114 | loading: state.auth.loading,
115 | error: state.auth.error,
116 | authenticated: state.auth.token !== null
117 | };
118 | };
119 |
120 | const mapDispatchToProps = dispatch => {
121 | return {
122 | login: (username, password) => dispatch(login(username, password))
123 | };
124 | };
125 |
126 | export default connect(
127 | mapStateToProps,
128 | mapDispatchToProps
129 | )(NormalLoginForm);
130 |
--------------------------------------------------------------------------------
/src/containers/Signup.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Button,
4 | Form,
5 | Grid,
6 | Header,
7 | Image,
8 | Message,
9 | Segment
10 | } from "semantic-ui-react";
11 | import { connect } from "react-redux";
12 | import { Link, Redirect } from "react-router-dom";
13 | import Logo from "../assets/images/logo.svg";
14 | import { authSignup as signup } from "../store/actions/auth";
15 |
16 | class RegistrationForm extends React.Component {
17 | state = {
18 | username: "",
19 | email: "",
20 | password: "",
21 | confirmPassword: "",
22 | formError: null
23 | };
24 |
25 | handleSubmit = e => {
26 | e.preventDefault();
27 | const { username, email, password, confirmPassword } = this.state;
28 | if (
29 | username !== "" &&
30 | email !== "" &&
31 | password !== "" &&
32 | confirmPassword !== "" &&
33 | this.comparePasswords() === true &&
34 | this.comparePasswordLengths() === true
35 | )
36 | this.props.signup(username, email, password, confirmPassword);
37 | };
38 |
39 | comparePasswords = () => {
40 | const { password, confirmPassword } = this.state;
41 | if (password !== confirmPassword) {
42 | this.setState({ formError: "Your passwords do not match" });
43 | return false;
44 | } else {
45 | return true;
46 | }
47 | };
48 |
49 | comparePasswordLengths = () => {
50 | const { password, confirmPassword } = this.state;
51 | if (password.length >= 6 && confirmPassword.length >= 6) {
52 | return true;
53 | } else {
54 | this.setState({
55 | formError: "Your password must be a minimum of 6 characters"
56 | });
57 | return false;
58 | }
59 | };
60 |
61 | handleChange = e => {
62 | this.setState({
63 | [e.target.name]: e.target.value,
64 | formError: null
65 | });
66 | };
67 |
68 | render() {
69 | const { formError } = this.state;
70 | const { loading, error, authenticated } = this.props;
71 | if (authenticated) {
72 | return ;
73 | }
74 | return (
75 |
76 |
81 |
82 |
83 | Create an account
84 |
85 |
133 | {formError && (
134 |
135 | There was an error
136 | {formError}
137 |
138 | )}
139 | {error && (
140 |
141 | There was an error
142 | {error}
143 |
144 | )}
145 |
146 | Already have an account? Login
147 |
148 |
149 |
150 |
151 | );
152 | }
153 | }
154 |
155 | const mapStateToProps = state => {
156 | return {
157 | loading: state.auth.loading,
158 | error: state.auth.error,
159 | authenticated: state.auth.token !== null
160 | };
161 | };
162 |
163 | const mapDispatchToProps = dispatch => {
164 | return {
165 | signup: (username, email, password1, password2) =>
166 | dispatch(signup(username, email, password1, password2))
167 | };
168 | };
169 |
170 | export default connect(
171 | mapStateToProps,
172 | mapDispatchToProps
173 | )(RegistrationForm);
174 |
--------------------------------------------------------------------------------
/src/hoc/hoc.js:
--------------------------------------------------------------------------------
1 | const Hoc = props => props.children;
2 |
3 | export default Hoc;
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import registerServiceWorker from "./registerServiceWorker";
5 | import { createStore, compose, applyMiddleware, combineReducers } from "redux";
6 | import { Provider } from "react-redux";
7 | import thunk from "redux-thunk";
8 |
9 | import authReducer from "./store/reducers/auth";
10 |
11 | const composeEnhances = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
12 |
13 | const rootReducer = combineReducers({
14 | auth: authReducer
15 | });
16 |
17 | const store = createStore(rootReducer, composeEnhances(applyMiddleware(thunk)));
18 |
19 | const app = (
20 |
21 |
22 |
23 | );
24 |
25 | ReactDOM.render(app, document.getElementById("root"));
26 | registerServiceWorker();
27 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === "localhost" ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === "[::1]" ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener("load", () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | "This web app is being served cache-first by a service " +
44 | "worker. To learn more, visit https://goo.gl/SC7cgQ"
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === "installed") {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log("New content is available; please refresh.");
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log("Content is cached for offline use.");
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error("Error during service worker registration:", error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get("content-type").indexOf("javascript") === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | "No internet connection found. App is running in offline mode."
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ("serviceWorker" in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Redirect } from "react-router-dom";
3 | import Hoc from "./hoc/hoc";
4 | import Login from "./containers/Login";
5 | import Signup from "./containers/Signup";
6 | import LandingPage from "./containers/LandingPage";
7 | import Demo from "./containers/Demo";
8 | import ChangeEmail from "./containers/Account/ChangeEmail";
9 | import ChangePassword from "./containers/Account/ChangePassword";
10 | import Billing from "./containers/Account/Billing";
11 | import APIKey from "./containers/Account/APIKey";
12 |
13 | const PrivateRoute = ({ component: Component, ...rest }) => {
14 | const authenticated = localStorage.getItem("token") !== null;
15 | return (
16 |
19 | authenticated === true ? (
20 |
21 | ) : (
22 |
28 | )
29 | }
30 | />
31 | );
32 | };
33 |
34 | const BaseRouter = () => (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export default BaseRouter;
48 |
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const AUTH_START = "AUTH_START";
2 | export const AUTH_SUCCESS = "AUTH_SUCCESS";
3 | export const AUTH_FAIL = "AUTH_FAIL";
4 | export const AUTH_LOGOUT = "AUTH_LOGOUT";
5 |
--------------------------------------------------------------------------------
/src/store/actions/auth.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import * as actionTypes from "./actionTypes";
3 | import { loginURL, signupURL } from "../../constants";
4 |
5 | export const authStart = () => {
6 | return {
7 | type: actionTypes.AUTH_START
8 | };
9 | };
10 |
11 | export const authSuccess = token => {
12 | return {
13 | type: actionTypes.AUTH_SUCCESS,
14 | token: token
15 | };
16 | };
17 |
18 | export const authFail = error => {
19 | return {
20 | type: actionTypes.AUTH_FAIL,
21 | error: error
22 | };
23 | };
24 |
25 | export const logout = () => {
26 | localStorage.removeItem("token");
27 | localStorage.removeItem("expirationDate");
28 | return {
29 | type: actionTypes.AUTH_LOGOUT
30 | };
31 | };
32 |
33 | export const checkAuthTimeout = expirationTime => {
34 | return dispatch => {
35 | setTimeout(() => {
36 | dispatch(logout());
37 | }, expirationTime * 1000);
38 | };
39 | };
40 |
41 | export const authLogin = (username, password) => {
42 | return dispatch => {
43 | dispatch(authStart());
44 | axios
45 | .post(loginURL, {
46 | username,
47 | password
48 | })
49 | .then(res => {
50 | const token = res.data.key;
51 | const expirationDate = new Date(new Date().getTime() + 3600 * 1000);
52 | localStorage.setItem("token", token);
53 | localStorage.setItem("expirationDate", expirationDate);
54 | dispatch(authSuccess(token));
55 | dispatch(checkAuthTimeout(3600));
56 | })
57 | .catch(err => {
58 | dispatch(authFail(err.response.data.non_field_errors[0]));
59 | });
60 | };
61 | };
62 |
63 | export const authSignup = (username, email, password1, password2) => {
64 | return dispatch => {
65 | dispatch(authStart());
66 | axios
67 | .post(signupURL, {
68 | username,
69 | email,
70 | password1,
71 | password2
72 | })
73 | .then(res => {
74 | const token = res.data.key;
75 | const expirationDate = new Date(new Date().getTime() + 3600 * 1000);
76 | localStorage.setItem("token", token);
77 | localStorage.setItem("expirationDate", expirationDate);
78 | dispatch(authSuccess(token));
79 | dispatch(checkAuthTimeout(3600));
80 | })
81 | .catch(err => {
82 | dispatch(authFail(err.response.data.non_field_errors[0]));
83 | });
84 | };
85 | };
86 |
87 | export const authCheckState = () => {
88 | return dispatch => {
89 | const token = localStorage.getItem("token");
90 | if (token === undefined) {
91 | dispatch(logout());
92 | } else {
93 | const expirationDate = new Date(localStorage.getItem("expirationDate"));
94 | if (expirationDate <= new Date()) {
95 | dispatch(logout());
96 | } else {
97 | dispatch(authSuccess(token));
98 | dispatch(
99 | checkAuthTimeout(
100 | (expirationDate.getTime() - new Date().getTime()) / 1000
101 | )
102 | );
103 | }
104 | }
105 | };
106 | };
107 |
--------------------------------------------------------------------------------
/src/store/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from "../actions/actionTypes";
2 | import { updateObject } from "../utility";
3 |
4 | const initialState = {
5 | token: null,
6 | error: null,
7 | loading: false
8 | };
9 |
10 | const authStart = (state, action) => {
11 | return updateObject(state, {
12 | error: null,
13 | loading: true
14 | });
15 | };
16 |
17 | const authSuccess = (state, action) => {
18 | return updateObject(state, {
19 | token: action.token,
20 | error: null,
21 | loading: false
22 | });
23 | };
24 |
25 | const authFail = (state, action) => {
26 | return updateObject(state, {
27 | error: action.error,
28 | loading: false
29 | });
30 | };
31 |
32 | const authLogout = (state, action) => {
33 | return updateObject(state, {
34 | token: null
35 | });
36 | };
37 |
38 | const reducer = (state = initialState, action) => {
39 | switch (action.type) {
40 | case actionTypes.AUTH_START:
41 | return authStart(state, action);
42 | case actionTypes.AUTH_SUCCESS:
43 | return authSuccess(state, action);
44 | case actionTypes.AUTH_FAIL:
45 | return authFail(state, action);
46 | case actionTypes.AUTH_LOGOUT:
47 | return authLogout(state, action);
48 | default:
49 | return state;
50 | }
51 | };
52 |
53 | export default reducer;
54 |
--------------------------------------------------------------------------------
/src/store/utility.js:
--------------------------------------------------------------------------------
1 | export const updateObject = (oldObject, updatedProperties) => {
2 | return {
3 | ...oldObject,
4 | ...updatedProperties
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { Responsive } from "semantic-ui-react";
2 | import axios from "axios";
3 | import { APIEndpoint } from "./constants";
4 |
5 | export const getWidth = () => {
6 | const isSSR = typeof window === "undefined";
7 | return isSSR ? Responsive.onlyTablet.minWidth : window.innerWidth;
8 | };
9 |
10 | export const authAxios = axios.create({
11 | baseURL: APIEndpoint,
12 | headers: {
13 | Authorization: {
14 | toString() {
15 | return `Token ${localStorage.getItem("token")}`;
16 | }
17 | }
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justdjango/facial-recognition-saas/8af024e02ade8d84997537a6220f327021f050a2/thumbnail.png
--------------------------------------------------------------------------------