├── .babelrc ├── .gitignore ├── .npmrc ├── Dockerfile ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tasks.py ├── tests.py ├── urls.py └── views.py ├── auth ├── __init__.py ├── serializers.py ├── urls.py ├── utils.py └── views.py ├── common ├── __init__.py ├── audit_log_middleware.py ├── paginations.py └── tests.py ├── django_tenant_react ├── __init__.py ├── asgi.py ├── celery.py ├── settings.py ├── urls.py ├── urls_public.py └── wsgi.py ├── docker-compose.yml ├── frontend ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── src │ ├── actions │ │ ├── AuthAction.js │ │ ├── InitAction.js │ │ ├── TodoAction.js │ │ ├── TodoGroupAction.js │ │ └── index.js │ ├── components │ │ ├── buttons │ │ │ ├── IconButton.js │ │ │ └── index.js │ │ ├── containers │ │ │ ├── InputFieldContainer.js │ │ │ └── index.js │ │ ├── header │ │ │ ├── Background.js │ │ │ ├── Header.js │ │ │ ├── NavLink.js │ │ │ └── index.js │ │ ├── inputs │ │ │ ├── BodyField.js │ │ │ ├── ConfirmPasswordField.js │ │ │ ├── GroupField.js │ │ │ ├── InputField.js │ │ │ ├── NameField.js │ │ │ ├── PasswordField.js │ │ │ ├── SearchField.js │ │ │ ├── TextAreaField.js │ │ │ ├── TitleInput.js │ │ │ ├── UsernameField.js │ │ │ └── index.js │ │ ├── modals │ │ │ ├── AddGroupModal.js │ │ │ ├── AddTodoModal.js │ │ │ ├── EditGroupModal.js │ │ │ └── index.js │ │ └── ui │ │ │ ├── Alert.js │ │ │ ├── BaseLoader.js │ │ │ ├── ErrorBackground.js │ │ │ ├── LandingLoader.js │ │ │ ├── MainLoader.js │ │ │ └── index.js │ ├── constants │ │ ├── Navigations.js │ │ ├── State.js │ │ ├── URLs.js │ │ └── index.js │ ├── hooks │ │ ├── GroupHooks.js │ │ └── index.js │ ├── index.js │ ├── locale │ │ ├── ar.json │ │ ├── en.json │ │ ├── i18n.js │ │ └── index.js │ ├── reducers │ │ ├── ConfigurationReducer.js │ │ ├── ErrorReducer.js │ │ ├── TodoGroupReducer.js │ │ ├── TodoReducer.js │ │ ├── UIReducer.js │ │ ├── UserReducer.js │ │ └── index.js │ ├── resources │ │ ├── images │ │ │ ├── 404.png │ │ │ ├── bg.jpg │ │ │ └── unauthorized.png │ │ └── theme │ │ │ └── colors.js │ ├── routers │ │ ├── ProtectedRouter.js │ │ ├── Routers.js │ │ ├── UnProtectedRouter.js │ │ └── index.js │ ├── screens │ │ ├── group │ │ │ └── GroupScreen.js │ │ ├── landing │ │ │ ├── LandingScreen.js │ │ │ └── components │ │ │ │ ├── LandingLoader.js │ │ │ │ ├── groups │ │ │ │ ├── CardListView.js │ │ │ │ ├── GroupCard.js │ │ │ │ ├── GroupsLists.js │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ └── not_logged_in │ │ │ │ ├── NotLoggedIn.js │ │ │ │ └── index.js │ │ ├── not_found │ │ │ └── NotFoundScreen.js │ │ ├── signin │ │ │ └── SigninScreen.js │ │ ├── signup │ │ │ └── SignupScreen.js │ │ ├── todo │ │ │ ├── TodoScreen.js │ │ │ └── components │ │ │ │ ├── TodoCard.js │ │ │ │ ├── TodoList.js │ │ │ │ └── index.js │ │ ├── todo_detail │ │ │ └── TodoDetailScreen.js │ │ └── unauthorized │ │ │ └── UnauthorizedScreen.js │ ├── services │ │ ├── apis │ │ │ ├── API.js │ │ │ ├── AuthAPIs.js │ │ │ ├── TodoAPIs.js │ │ │ ├── TodoGroupAPIs.js │ │ │ └── index.js │ │ └── storages │ │ │ ├── Auth.js │ │ │ ├── User.js │ │ │ ├── index.js │ │ │ └── language.js │ └── utils │ │ ├── errors │ │ ├── AuthError.js │ │ ├── GeneralError.js │ │ └── index.js │ │ ├── formatter │ │ ├── User.js │ │ └── index.js │ │ ├── index.js │ │ ├── utils.js │ │ └── validators │ │ ├── Auth.js │ │ ├── Email.js │ │ ├── Name.js │ │ ├── Password.js │ │ ├── Username.js │ │ └── index.js ├── static │ └── frontend │ │ └── css │ │ └── style.css ├── templates │ └── frontend │ │ └── index.html ├── tests.py ├── urls.py └── views.py ├── locale └── ar │ └── LC_MESSAGES │ └── django.po ├── manage.py ├── package.json ├── requirements.txt ├── tenants ├── __init__.py ├── admin.py ├── apps.py ├── middleware │ ├── __init__.py │ └── main.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── todo ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | "transform-class-properties" 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | frontend/static/frontend/js/* 3 | package-lock.json 4 | webpack.config.prod.js 5 | 6 | # Created by https://www.toptal.com/developers/gitignore/api/django 7 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 8 | 9 | ### Django ### 10 | *.log 11 | *.pot 12 | *.pyc 13 | __pycache__/ 14 | local_settings.py 15 | db.sqlite3 16 | db.sqlite3-journal 17 | media 18 | debug.log 19 | 20 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 21 | # in your Git repository. Update and uncomment the following line accordingly. 22 | # /staticfiles/ 23 | 24 | ### Django.Python Stack ### 25 | # Byte-compiled / optimized / DLL files 26 | *.py[cod] 27 | *$py.class 28 | 29 | # C extensions 30 | *.so 31 | 32 | # Distribution / packaging 33 | .Python 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | pip-wheel-metadata/ 47 | share/python-wheels/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | MANIFEST 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .nox/ 67 | .coverage 68 | .coverage.* 69 | .cache 70 | nosetests.xml 71 | coverage.xml 72 | *.cover 73 | *.py,cover 74 | .hypothesis/ 75 | .pytest_cache/ 76 | pytestdebug.log 77 | 78 | # Translations 79 | *.mo 80 | 81 | # Django stuff: 82 | 83 | # Flask stuff: 84 | instance/ 85 | .webassets-cache 86 | 87 | # Scrapy stuff: 88 | .scrapy 89 | 90 | # Sphinx documentation 91 | docs/_build/ 92 | doc/_build/ 93 | 94 | # PyBuilder 95 | target/ 96 | 97 | # Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | # IPython 101 | profile_default/ 102 | ipython_config.py 103 | 104 | # pyenv 105 | .python-version 106 | 107 | # pipenv 108 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 109 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 110 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 111 | # install all needed dependencies. 112 | #Pipfile.lock 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | pythonenv* 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # profiling data 156 | .prof 157 | 158 | # End of https://www.toptal.com/developers/gitignore/api/django -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nikolaik/python-nodejs:python3.8-nodejs14 2 | ENV PYTHONUNBUFFERED 1 3 | RUN mkdir /app 4 | WORKDIR /app 5 | ADD . /app/ 6 | RUN python -m pip install --upgrade pip 7 | RUN pip install -r requirements.txt 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Tenant React 🐍 2 | this is a simple todo list application, written with django and django rest framework, 3 | support multitenancy and auditlog, and the front-end is written in ReactJS, 4 | and for the state management Redux and React Context API are used. 5 | 6 | # **getting started 🚦** 7 | 8 | - install docker 9 | - install docker-compose 10 | - clone the repo 11 | - initializing the app 12 | 13 | **installing docker 🐋** 14 | 15 | you can follow the steps [here](https://docs.docker.com/install/). 16 | they should be pretty simple :) 17 | 18 | **installing docker-compose 🚢** 19 | 20 | follow the steps [here](https://docs.docker.com/compose/install/) 21 | 22 | **clone the repo** 23 | 24 | git clone https://github.com/majid-cj/django_tenant_react.git 25 | 26 | **initializing the app 🔨** 27 | 28 | first you need to edit your hosts file in your machine add the following line. 29 | 30 | 127.0.0.1 djangotenant.com 31 | 32 | whenever you create an account in order to access it you need to append the hosts file with following pattern 33 | 34 | 127.0.0.1 [account_username].djangotenant.com 35 | 36 | **docker-compose up** 37 | 38 | # that's it 😁 39 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Account 3 | 4 | # Register your models here. 5 | admin.site.register(Account) 6 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /accounts/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import BaseUserManager 2 | 3 | 4 | class AccountManager(BaseUserManager): 5 | def create_superuser(self, name, username, password): 6 | user = self.model(name=name, username=username, password=password) 7 | user.set_password(password) 8 | user.is_active = True 9 | user.is_staff = True 10 | user.is_superuser = True 11 | user.save(using=self._db) 12 | return user 13 | 14 | def create_user(self, name, username, password): 15 | user = self.model(name=name, username=username, password=password) 16 | user.set_password(password) 17 | user.is_active = True 18 | user.is_staff = False 19 | user.is_superuser = False 20 | user.save(using=self._db) 21 | return user 22 | 23 | def get_by_natural_key(self, username_): 24 | return self.get(username=username_) 25 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.16 on 2021-01-19 08:38 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0011_update_proxy_permissions'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Account', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('password', models.CharField(max_length=128, verbose_name='password')), 22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 23 | ('name', models.CharField(max_length=50)), 24 | ('username', models.CharField(max_length=25, unique=True, validators=[django.core.validators.RegexValidator(message='invalid username formate', regex='^[a-z0-9+]{2,25}$')])), 25 | ('is_active', models.BooleanField(default=True)), 26 | ('is_staff', models.BooleanField(default=False)), 27 | ('is_superuser', models.BooleanField(default=False)), 28 | ('created_date', models.DateTimeField(default=django.utils.timezone.now)), 29 | ('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')), 30 | ('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')), 31 | ], 32 | options={ 33 | 'abstract': False, 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser 4 | from django.core.validators import RegexValidator 5 | from django.utils import timezone 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from django.db import models 9 | 10 | from .managers import AccountManager 11 | 12 | 13 | # Create your models here. 14 | USERNAME_PATTERN = "^[a-z0-9+]{2,25}$" 15 | USERNAME_ERROR_MESSAGE = _("invalid username formate") 16 | 17 | 18 | class Account(PermissionsMixin, AbstractBaseUser): 19 | name = models.CharField(max_length=50) 20 | username = models.CharField( 21 | max_length=25, 22 | unique=True, 23 | validators=[ 24 | RegexValidator(regex=USERNAME_PATTERN, message=USERNAME_ERROR_MESSAGE) 25 | ], 26 | ) 27 | is_active = models.BooleanField(default=True) 28 | is_staff = models.BooleanField(default=False) 29 | is_superuser = models.BooleanField(default=False) 30 | created_date = models.DateTimeField(default=timezone.now) 31 | 32 | REQUIRED_FIELDS = ["name", "password"] 33 | USERNAME_FIELD = "username" 34 | 35 | objects = AccountManager() 36 | -------------------------------------------------------------------------------- /accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .models import Account 5 | 6 | 7 | class AccountSerializer(serializers.ModelSerializer): 8 | confirm_password = serializers.CharField(required=True) 9 | 10 | class Meta: 11 | model = Account 12 | fields = ["name", "username", "password", "confirm_password"] 13 | extra_kwargs = { 14 | "password": {"write_only": True}, 15 | "confirm_password": {"write_only": True}, 16 | } 17 | 18 | def validate(self, attrs): 19 | if attrs["password"] != attrs["confirm_password"]: 20 | raise serializers.ValidationError(_("passwords don't match")) 21 | return super().validate(attrs) 22 | 23 | def create(self, validated_data): 24 | is_superuser = validated_data.get("is_superuser", None) 25 | if is_superuser: 26 | return Account.objects.create_superuser( 27 | name=validated_data["name"], 28 | username=validated_data["username"], 29 | password=validated_data["password"], 30 | ) 31 | 32 | return Account.objects.create_superuser( 33 | name=validated_data["name"], 34 | username=validated_data["username"], 35 | password=validated_data["password"], 36 | ) 37 | -------------------------------------------------------------------------------- /accounts/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from celery import shared_task 4 | from django.db import transaction 5 | from django_tenants.utils import connection 6 | 7 | from tenants.models import Tenant, Domain 8 | from .serializers import AccountSerializer 9 | 10 | from django_tenant_react.settings import APPLICATION_DOMAIN 11 | 12 | 13 | @transaction.atomic 14 | @shared_task 15 | def create_account(data): 16 | account = AccountSerializer(data=data) 17 | account.is_valid(raise_exception=True) 18 | connection.set_schema_to_public() 19 | username = data['username'] 20 | tenant = Tenant.objects.create(schema_name=username) 21 | connection.set_tenant(tenant) 22 | domain = Domain() 23 | domain.tenant = tenant 24 | domain.domain = f"{username}.{APPLICATION_DOMAIN}" 25 | account.save() 26 | domain.save() 27 | 28 | 29 | @shared_task 30 | def send_log_in_email(email): 31 | pass 32 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django_tenants.utils import connection 2 | 3 | from rest_framework.test import APITestCase 4 | from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK 5 | 6 | from common.tests import DjangoReduxTestCases 7 | 8 | from .models import Account 9 | 10 | # Create your tests here. 11 | 12 | 13 | class AccountModelTests(DjangoReduxTestCases): 14 | 15 | def test_create_account_for_tenants(self): 16 | accounts = Account.objects.all() 17 | self.assertEquals(accounts.count(), 1) 18 | 19 | 20 | class AccountViewTests(APITestCase, DjangoReduxTestCases): 21 | 22 | def test_sign_up_api(self): 23 | response = self.client.post(self.signup_url, self.signup) 24 | self.assertEquals(response.status_code, HTTP_201_CREATED) 25 | 26 | def test_token_api(self): 27 | response = self.client.post(self.signup_url, self.signup) 28 | self.assertEquals(response.status_code, HTTP_201_CREATED) 29 | 30 | response = self.client.post(self.token_url, self.signin) 31 | self.assertEquals(response.status_code, HTTP_200_OK) 32 | self.assertEquals(response.data['username'], self.signin['username']) 33 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | app_name = 'accounts' 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/auth/__init__.py -------------------------------------------------------------------------------- /auth/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import update_last_login 2 | 3 | from rest_framework_simplejwt.tokens import RefreshToken 4 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 5 | 6 | 7 | class DjangoReduxJWTSerializers(TokenObtainPairSerializer): 8 | 9 | @classmethod 10 | def get_token(cls, user): 11 | return RefreshToken.for_user(user) 12 | 13 | def validate(self, attrs): 14 | data = super().validate(attrs) 15 | refresh = self.get_token(self.user) 16 | 17 | data['refresh'] = str(refresh) 18 | data['access'] = str(refresh.access_token) 19 | data['id'] = self.user.pk 20 | data['username'] = self.user.get_username() 21 | 22 | update_last_login(None, self.user) 23 | 24 | return data 25 | -------------------------------------------------------------------------------- /auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenRefreshView 3 | 4 | from .views import DjangoReduxObtainToken, sign_up 5 | 6 | 7 | urlpatterns = [ 8 | path(r'token/', DjangoReduxObtainToken.as_view(), name='sign_in'), 9 | path(r'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 10 | path(r'signup/', sign_up, name='sign_up'), 11 | ] 12 | -------------------------------------------------------------------------------- /auth/utils.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | from django.utils.translation import gettext_lazy as _ 4 | from django.contrib.auth import get_user_model 5 | from django.conf import settings 6 | 7 | from rest_framework.authentication import BaseAuthentication 8 | from rest_framework.exceptions import AuthenticationFailed 9 | 10 | 11 | class DjangoReduxJWTAuthentication(BaseAuthentication): 12 | 13 | def authenticate(self, request): 14 | User = get_user_model() 15 | authorization_heaader = request.headers.get("Authorization") 16 | 17 | if not authorization_heaader: 18 | return None 19 | try: 20 | access_token = authorization_heaader.split(" ")[1] 21 | payload = jwt.decode( 22 | access_token, settings.SECRET_KEY, algorithms=["HS256"] 23 | ) 24 | 25 | except jwt.ExpiredSignatureError: 26 | raise AuthenticationFailed(_("access token expired")) 27 | except IndexError: 28 | raise AuthenticationFailed(_("token prefix missing")) 29 | 30 | user = User.objects.filter(id=payload["user_id"]).first() 31 | if user is None: 32 | raise AuthenticationFailed(_("user not found")) 33 | 34 | if not user.is_active: 35 | raise AuthenticationFailed(_("user is inactive")) 36 | 37 | return user, payload 38 | -------------------------------------------------------------------------------- /auth/views.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext_lazy as _ 2 | 3 | from rest_framework.decorators import api_view 4 | from rest_framework.views import Response 5 | from rest_framework.permissions import AllowAny 6 | from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_201_CREATED 7 | from rest_framework_simplejwt.views import TokenObtainPairView 8 | 9 | from accounts.models import Account 10 | from accounts.serializers import AccountSerializer 11 | from tenants.models import Tenant 12 | from .serializers import DjangoReduxJWTSerializers 13 | 14 | from accounts.tasks import create_account, send_log_in_email 15 | 16 | # Create your views here. 17 | 18 | 19 | class DjangoReduxObtainToken(TokenObtainPairView): 20 | permission_classes = [AllowAny] 21 | serializer_class = DjangoReduxJWTSerializers 22 | 23 | 24 | @api_view(["POST"]) 25 | def sign_up(request): 26 | serializer = AccountSerializer(data=request.data) 27 | serializer.is_valid(raise_exception=True) 28 | 29 | find_one = Tenant.objects.filter(schema_name=request.data.get("username")) 30 | if find_one.first(): 31 | return Response( 32 | status=HTTP_400_BAD_REQUEST, data=_("this username is already exists") 33 | ) 34 | 35 | create_account(data=request.data) 36 | send_log_in_email(email=request.data.get("email")) 37 | return Response(status=HTTP_201_CREATED, data=_("account created login next")) 38 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/common/__init__.py -------------------------------------------------------------------------------- /common/audit_log_middleware.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from functools import partial as curry 5 | 6 | from django.conf import settings 7 | from django.db.models.signals import pre_save 8 | from django.apps import apps 9 | from auditlog.models import LogEntry 10 | 11 | from auth.utils import DjangoReduxJWTAuthentication 12 | 13 | from django.utils.deprecation import MiddlewareMixin 14 | 15 | thread_local = threading.local() 16 | 17 | 18 | class AuditLogMiddleware(MiddlewareMixin): 19 | 20 | def process_request(self, request): 21 | # Initialize thread local storage 22 | thread_local.audit_log = { 23 | "signal_duid": (self.__class__, time.time()), 24 | "remote_addr": request.META.get("REMOTE_ADDR"), 25 | } 26 | 27 | if request.META.get("HTTP_X_FORWARDED_FOR"): 28 | thread_local.audit_log["remote_addr"] = request.META.get( 29 | "HTTP_X_FORWARDED_FOR" 30 | ).split(",")[0] 31 | 32 | auth = DjangoReduxJWTAuthentication().authenticate(request) 33 | 34 | if auth: 35 | user, _ = auth 36 | set_actor = curry( 37 | self.set_actor, 38 | user=user, 39 | signal_duid=thread_local.audit_log["signal_duid"], 40 | ) 41 | pre_save.connect( 42 | set_actor, 43 | sender=LogEntry, 44 | dispatch_uid=thread_local.audit_log["signal_duid"], 45 | weak=False, 46 | ) 47 | 48 | def process_response(self, request, response): 49 | if hasattr(thread_local, "audit_log"): 50 | pre_save.disconnect( 51 | sender=LogEntry, dispatch_uid=thread_local.audit_log["signal_duid"] 52 | ) 53 | 54 | return response 55 | 56 | def process_exception(self, request, exception): 57 | if hasattr(thread_local, "audit_log"): 58 | pre_save.disconnect( 59 | sender=LogEntry, dispatch_uid=thread_local.audit_log["signal_duid"] 60 | ) 61 | 62 | return None 63 | 64 | @staticmethod 65 | def set_actor(user, sender, instance, signal_duid, **kwargs): 66 | if hasattr(thread_local, "audit_log"): 67 | if signal_duid != thread_local.audit_log["signal_duid"]: 68 | return 69 | try: 70 | app_label, model_name = settings.AUTH_USER_MODEL.split(".") 71 | auth_user_model = apps.get_model(app_label, model_name) 72 | except ValueError: 73 | auth_user_model = apps.get_model("auth", "user") 74 | if ( 75 | sender == LogEntry 76 | and isinstance(user, auth_user_model) 77 | and instance.actor is None 78 | ): 79 | instance.actor = user 80 | 81 | instance.remote_addr = thread_local.audit_log["remote_addr"] 82 | -------------------------------------------------------------------------------- /common/paginations.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class DjangoReduxPagination(PageNumberPagination): 5 | page_size = 9 6 | page_size_query_param = 'size' 7 | max_page_size = 25 8 | -------------------------------------------------------------------------------- /common/tests.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test import TestCase 3 | from django_tenants.utils import connection 4 | 5 | from accounts.models import Account 6 | from tenants.models import Tenant, Domain 7 | 8 | 9 | class DjangoReduxTestCases(TestCase): 10 | signup_url = f'http://{settings.APPLICATION_DOMAIN}:8000/auth/signup/' 11 | token_url = f'http://{settings.APPLICATION_DOMAIN}:8000/auth/token/' 12 | 13 | signup = { 14 | 'name': 'test user', 15 | 'username': 'test', 16 | 'password': 'm1122335544', 17 | 'confirm_password': 'm1122335544', 18 | } 19 | signin = { 20 | 'username': 'test', 21 | 'password': 'm1122335544', 22 | } 23 | 24 | def setUp(self): 25 | connection.set_schema_to_public() 26 | self.tenant.refresh_from_db() 27 | connection.set_tenant(self.tenant) 28 | self.account.refresh_from_db() 29 | 30 | @classmethod 31 | def setUpTestData(cls): 32 | connection.set_schema_to_public() 33 | cls.tenant = Tenant.objects.create(schema_name='tenant1') 34 | cls.tenant_domain = Domain() 35 | cls.tenant_domain.tenant = cls.tenant 36 | cls.tenant_domain.domain = 'tenant1.djangoredux.com' 37 | cls.tenant_domain.save() 38 | 39 | connection.set_tenant(cls.tenant) 40 | cls.account = Account.objects.create_superuser( 41 | 'tenant user', 'user', 'Tu112231') 42 | 43 | connection.set_schema_to_public() 44 | 45 | def get_url(self, tenant, view): 46 | return f'http://{tenant}.{settings.APPLICATION_DOMAIN}:8000{view}' 47 | -------------------------------------------------------------------------------- /django_tenant_react/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ['celery_app'] 4 | -------------------------------------------------------------------------------- /django_tenant_react/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_tenant_react project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_tenant_react.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /django_tenant_react/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import os 3 | from celery import Celery 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_tenant_react.settings') 6 | 7 | app = Celery('django_tenant_react') 8 | 9 | app.config_from_object('django.conf:settings', namespace='CELERY') 10 | 11 | app.autodiscover_tasks() 12 | -------------------------------------------------------------------------------- /django_tenant_react/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_tenant_react project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.16. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from django.utils.translation import gettext_lazy as _ 14 | 15 | from pathlib import Path 16 | from datetime import timedelta 17 | 18 | import os 19 | 20 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 21 | BASE_DIR = Path(__file__).resolve().parent.parent 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = "&g(@2zfzmxmsds4a*#j$&0n9#k*_ni=c35y--mz15)9j)=%^_=" 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = True 32 | 33 | ALLOWED_HOSTS = ["*"] 34 | 35 | 36 | # Application definition 37 | 38 | SHARED_APPS = [ 39 | "django_tenants", 40 | "tenants", 41 | "jazzmin", 42 | "django.contrib.admin", 43 | "django.contrib.auth", 44 | "django.contrib.contenttypes", 45 | "django.contrib.sessions", 46 | "django.contrib.messages", 47 | "django.contrib.staticfiles", 48 | "accounts", 49 | ] 50 | 51 | TENANT_APPS = [ 52 | "django.contrib.auth", 53 | "django.contrib.contenttypes", 54 | "django.contrib.sessions", 55 | "django.contrib.messages", 56 | "rest_framework", 57 | "django_filters", 58 | "auditlog", 59 | "corsheaders", 60 | "frontend", 61 | "todo", 62 | ] 63 | 64 | TENANT_MODEL = "tenants.Tenant" 65 | TENANT_DOMAIN_MODEL = "tenants.Domain" 66 | 67 | AUTH_USER_MODEL = "accounts.Account" 68 | 69 | INSTALLED_APPS = list(set(SHARED_APPS + TENANT_APPS)) 70 | 71 | MIDDLEWARE = [ 72 | "tenants.middleware.main.TenantMainMiddleware", 73 | "corsheaders.middleware.CorsMiddleware", 74 | "django.middleware.security.SecurityMiddleware", 75 | "django.contrib.sessions.middleware.SessionMiddleware", 76 | "django.middleware.locale.LocaleMiddleware", 77 | "common.audit_log_middleware.AuditLogMiddleware", 78 | "django.middleware.common.CommonMiddleware", 79 | "django.middleware.csrf.CsrfViewMiddleware", 80 | "django.contrib.auth.middleware.AuthenticationMiddleware", 81 | "django.contrib.messages.middleware.MessageMiddleware", 82 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 83 | ] 84 | 85 | ROOT_URLCONF = "django_tenant_react.urls" 86 | PUBLIC_SCHEMA_URLCONF = "django_tenant_react.urls_public" 87 | 88 | CORS_ORIGIN_ALLOW_ALL = True 89 | CORS_ALLOW_HEADERS = [ 90 | "Accept", 91 | "Accept-Language", 92 | "Authorization", 93 | "Content-Type", 94 | ] 95 | CORS_ALLOW_METHODS = [ 96 | "GET", 97 | "POST", 98 | "PUT", 99 | "DELETE", 100 | ] 101 | 102 | 103 | TEMPLATES = [ 104 | { 105 | "BACKEND": "django.template.backends.django.DjangoTemplates", 106 | "DIRS": [], 107 | "APP_DIRS": True, 108 | "OPTIONS": { 109 | "context_processors": [ 110 | "django.template.context_processors.request", 111 | "django.template.context_processors.debug", 112 | "django.contrib.auth.context_processors.auth", 113 | "django.contrib.messages.context_processors.messages", 114 | ], 115 | }, 116 | }, 117 | ] 118 | 119 | WSGI_APPLICATION = "django_tenant_react.wsgi.application" 120 | 121 | 122 | # Database 123 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 124 | 125 | DATABASES = { 126 | "default": { 127 | "ENGINE": "django_tenants.postgresql_backend", 128 | "NAME": os.getenv("DB_NAME", "postgres"), 129 | "USER": os.getenv("DB_USER", "postgres"), 130 | "PASSWORD": os.getenv("DB_PASSWORD", "postgres"), 131 | "HOST": os.getenv("DB_HOST", "postgres"), 132 | "PORT": os.getenv("DB_PORT", 5432), 133 | } 134 | } 135 | 136 | DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"] 137 | 138 | 139 | # Password validation 140 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 141 | 142 | AUTH_PASSWORD_VALIDATORS = [ 143 | { 144 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 145 | }, 146 | { 147 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 148 | }, 149 | { 150 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 151 | }, 152 | { 153 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 154 | }, 155 | ] 156 | 157 | AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] 158 | 159 | REST_FRAMEWORK = { 160 | "DEFAULT_AUTHENTICATION_CLASSES": ["auth.utils.DjangoReduxJWTAuthentication"], 161 | "DEFAULT_PAGINATION_CLASS": "common.paginations.DjangoReduxPagination", 162 | } 163 | 164 | 165 | # Internationalization 166 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 167 | 168 | LANGUAGE_CODE = "en" 169 | 170 | LANGUAGES = [ 171 | ("en", _("English")), 172 | ("ar", _("Arabic")), 173 | ] 174 | 175 | LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] 176 | 177 | TIME_ZONE = os.getenv("TIME_ZONE", "Africa/Khartoum") 178 | 179 | USE_I18N = True 180 | 181 | USE_L10N = True 182 | 183 | USE_TZ = True 184 | 185 | 186 | # Static files (CSS, JavaScript, Images) 187 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 188 | 189 | STATIC_URL = "/static/" 190 | 191 | CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "amqp://rabbitmq") 192 | CELERY_IMPORTS = ["accounts.tasks"] 193 | 194 | # SimpleJWT Configurations 195 | 196 | SIMPLE_JWT = { 197 | "ACCESS_TOKEN_LIFETIME": timedelta(hours=12), 198 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 199 | "ROTATE_REFRESH_TOKENS": True, 200 | "BLACKLIST_AFTER_ROTATION": True, 201 | "UPDATE_LAST_LOGIN": True, 202 | "ALGORITHM": "HS256", 203 | "SIGNING_KEY": SECRET_KEY, 204 | "VERIFYING_KEY": None, 205 | "AUDIENCE": None, 206 | "ISSUER": None, 207 | "AUTH_HEADER_TYPES": ("Bearer",), 208 | "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 209 | "USER_ID_FIELD": "id", 210 | "USER_ID_CLAIM": "user_id", 211 | "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), 212 | "TOKEN_TYPE_CLAIM": "token_type", 213 | "JTI_CLAIM": "jti", 214 | "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", 215 | "SLIDING_TOKEN_LIFETIME": timedelta(hours=12), 216 | "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), 217 | } 218 | 219 | 220 | JAZZMIN_SETTINGS = { 221 | "language_chooser": True, 222 | } 223 | 224 | APPLICATION_DOMAIN = os.getenv("APPLICATION_DOMAIN", "djangotenant.com") 225 | 226 | LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO") 227 | LOGGING = { 228 | "version": 1, 229 | "disable_existing_loggers": False, 230 | "handlers": { 231 | "console": { 232 | "class": "logging.StreamHandler", 233 | }, 234 | "file": { 235 | "level": LOG_LEVEL, 236 | "class": "logging.FileHandler", 237 | "filename": "./debug.log", 238 | }, 239 | }, 240 | "root": { 241 | "handlers": ["console"], 242 | "level": "WARNING", 243 | }, 244 | "loggers": { 245 | "django": { 246 | "handlers": ["console", "file"], 247 | "level": LOG_LEVEL, 248 | "propagate": False, 249 | }, 250 | }, 251 | } 252 | -------------------------------------------------------------------------------- /django_tenant_react/urls.py: -------------------------------------------------------------------------------- 1 | """django_tenant_react URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from django.conf.urls.i18n import i18n_patterns 19 | 20 | urlpatterns = [ 21 | path(r'i18n/', include('django.conf.urls.i18n')), 22 | ] 23 | 24 | urlpatterns += i18n_patterns ( 25 | path(r'admin/', admin.site.urls), 26 | path(r'todo/', include('todo.urls', namespace='todo')), 27 | ) 28 | -------------------------------------------------------------------------------- /django_tenant_react/urls_public.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from django.conf.urls.i18n import i18n_patterns 3 | 4 | urlpatterns = [ 5 | path(r'', include('frontend.urls')), 6 | path(r'i18n/', include('django.conf.urls.i18n')), 7 | ] 8 | 9 | 10 | urlpatterns += i18n_patterns ( 11 | path(r'accounts/', include('accounts.urls')), 12 | path(r'auth/', include('auth.urls')), 13 | ) 14 | -------------------------------------------------------------------------------- /django_tenant_react/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_tenant_react project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_tenant_react.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | environment: 7 | POSTGRES_PASSWORD: postgres 8 | POSTGRES_USER: postgres 9 | ports: 10 | - '5434:5432' 11 | 12 | rabbitmq: 13 | image: rabbitmq 14 | 15 | web: &web 16 | build: . 17 | restart: on-failure 18 | command: bash -c "python manage.py migrate_schemas && python manage.py runserver 0.0.0.0:8000" 19 | volumes: 20 | - .:/app 21 | ports: 22 | - '8000:8000' 23 | depends_on: 24 | - postgres 25 | - rabbitmq 26 | - celery_worker 27 | 28 | celery_worker: 29 | <<: *web 30 | command: bash -c "celery -A django_tenant_react worker --loglevel=info" 31 | ports: [] 32 | depends_on: 33 | - rabbitmq 34 | 35 | frontend: 36 | <<: *web 37 | command: bash -c "npm run dev" 38 | ports: [] 39 | -------------------------------------------------------------------------------- /frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/frontend/__init__.py -------------------------------------------------------------------------------- /frontend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /frontend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrontendConfig(AppConfig): 5 | name = 'frontend' 6 | -------------------------------------------------------------------------------- /frontend/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/frontend/migrations/__init__.py -------------------------------------------------------------------------------- /frontend/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /frontend/src/actions/AuthAction.js: -------------------------------------------------------------------------------- 1 | import { LOGGED_IN, LOG_OUT, SET_ERROR, SET_USER } from '../constants'; 2 | import { refreshToken, signinAPI, signupAPI } from '../services/apis/AuthAPIs'; 3 | import { deleteToken, setToken } from '../services/storages/Auth'; 4 | import { deleteUser, setUser } from '../services/storages/User'; 5 | import { debounce, doNothing } from '../utils/utils'; 6 | import { showMainLoader } from './InitAction'; 7 | 8 | export const signinUser = (signinData, callBack = doNothing) => async (dispatch) => { 9 | dispatch(showMainLoader(true, 0)); 10 | dispatch(clearAuthErrors()); 11 | try { 12 | const response = await signinAPI(signinData); 13 | 14 | const { access, refresh, id, username } = response; 15 | 16 | const user = { id: id, username: username }; 17 | const token = { access: access, refresh: refresh }; 18 | 19 | setToken(token); 20 | setUser(user); 21 | 22 | dispatch({ type: SET_USER, value: { ...user } }); 23 | dispatch({ type: LOGGED_IN, value: true }); 24 | 25 | debounce(() => { 26 | callBack(); 27 | })(); 28 | } catch (error) { 29 | dispatch({ type: SET_ERROR, value: error }); 30 | } finally { 31 | dispatch(showMainLoader()); 32 | dispatch(clearAuthErrors()); 33 | } 34 | }; 35 | 36 | export const signupUser = (signupData, callBack = doNothing) => async (dispatch) => { 37 | dispatch(showMainLoader(true, 0)); 38 | dispatch(clearAuthErrors()); 39 | try { 40 | await signupAPI(signupData); 41 | debounce(() => { 42 | callBack(); 43 | })(); 44 | } catch (error) { 45 | dispatch({ type: SET_ERROR, value: error }); 46 | } finally { 47 | dispatch(showMainLoader()); 48 | dispatch(clearAuthErrors()); 49 | } 50 | }; 51 | 52 | export const refreshUserToken = () => async (dispatch) => { 53 | dispatch(clearAuthErrors()); 54 | try { 55 | const { refresh_token, access_token } = await refreshToken(); 56 | const token = { access: access_token, refresh: refresh_token }; 57 | setToken(token); 58 | } catch (error) { 59 | dispatch({ type: SET_ERROR, value: error }); 60 | } 61 | }; 62 | 63 | export const signoutUser = (callBack = doNothing) => (dispatch) => { 64 | deleteUser(); 65 | deleteToken(); 66 | dispatch({ type: LOG_OUT }); 67 | dispatch({ type: LOGGED_IN, value: false }); 68 | 69 | debounce(() => { 70 | callBack(); 71 | })(); 72 | }; 73 | 74 | export const clearAuthErrors = (timeout = 5000) => (dispatch) => { 75 | const error = { code: 300, message: null }; 76 | debounce(() => { 77 | dispatch({ type: SET_ERROR, value: error }); 78 | }, timeout)(); 79 | }; 80 | -------------------------------------------------------------------------------- /frontend/src/actions/InitAction.js: -------------------------------------------------------------------------------- 1 | import { AUTH_LOADER, LOGGED_IN, SET_ERROR, SET_USER } from '../constants'; 2 | import { getToken } from '../services/storages/Auth'; 3 | import { getUser } from '../services/storages/User'; 4 | import { debounce } from '../utils/utils'; 5 | 6 | export const initApp = () => (dispatch) => { 7 | const user = getUser(); 8 | const token = getToken(); 9 | 10 | dispatch({ type: SET_USER, value: { user: user, token: token } }); 11 | dispatch({ type: LOGGED_IN, value: user ? true : false }); 12 | }; 13 | 14 | export const setAuthErrors = (message) => (dispatch) => { 15 | const error = { code: 300, message: message }; 16 | dispatch({ type: SET_ERROR, value: error }); 17 | }; 18 | 19 | export const showMainLoader = (show = false, timeout = 75) => (dispatch) => { 20 | debounce(() => { 21 | dispatch({ type: AUTH_LOADER, value: show }); 22 | }, timeout)(); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/actions/TodoAction.js: -------------------------------------------------------------------------------- 1 | import { SET_ERROR, TODO_URL } from '../constants'; 2 | import { createAPI, deleteAPI, updateAPI } from '../services/apis/TodoGroupAPIs'; 3 | import { debounce, doNothing } from '../utils/utils'; 4 | import { showMainLoader } from './InitAction'; 5 | 6 | export const createTodo = (requestData, callback = doNothing) => async (dispatch) => { 7 | dispatch(showMainLoader(true, 0)); 8 | dispatch(clearTodoErrors()); 9 | try { 10 | await createAPI(requestData, TODO_URL); 11 | 12 | debounce(() => { 13 | callback(); 14 | })(); 15 | } catch (error) { 16 | dispatch({ type: SET_ERROR, value: error }); 17 | } finally { 18 | dispatch(showMainLoader()); 19 | } 20 | }; 21 | 22 | export const updateTodo = (requestData) => async (dispatch) => { 23 | dispatch(showMainLoader(true, 0)); 24 | dispatch(clearTodoErrors()); 25 | try { 26 | await updateAPI(requestData, TODO_URL); 27 | } catch (error) { 28 | dispatch({ type: SET_ERROR, value: error }); 29 | } finally { 30 | dispatch(showMainLoader()); 31 | } 32 | }; 33 | 34 | export const deleteTodo = (id) => async (dispatch) => { 35 | dispatch(showMainLoader(true, 0)); 36 | dispatch(clearTodoErrors()); 37 | try { 38 | await deleteAPI(id, TODO_URL); 39 | } catch (error) { 40 | dispatch({ type: SET_ERROR, value: error }); 41 | } finally { 42 | dispatch(showMainLoader()); 43 | } 44 | }; 45 | 46 | export const clearTodoErrors = () => (dispatch) => { 47 | const error = { code: 200, message: null }; 48 | dispatch({ type: SET_ERROR, value: error }); 49 | }; 50 | -------------------------------------------------------------------------------- /frontend/src/actions/TodoGroupAction.js: -------------------------------------------------------------------------------- 1 | import { SET_ERROR, TODO_GROUP_URL } from '../constants'; 2 | import { createAPI, updateAPI } from '../services/apis/TodoGroupAPIs'; 3 | import { debounce, doNothing } from '../utils/utils'; 4 | import { showMainLoader } from './InitAction'; 5 | 6 | export const createTodoGroups = (requestData, callback = doNothing) => async (dispatch) => { 7 | dispatch(showMainLoader(true, 0)); 8 | dispatch(clearTodoGroupErrors()); 9 | try { 10 | await createAPI(requestData); 11 | 12 | debounce(() => { 13 | callback(); 14 | })(); 15 | } catch (error) { 16 | dispatch({ type: SET_ERROR, value: error }); 17 | } finally { 18 | dispatch(showMainLoader()); 19 | } 20 | }; 21 | 22 | export const updateTodoGroups = (requestData, callback = doNothing) => async (dispatch) => { 23 | dispatch(showMainLoader(true, 0)); 24 | dispatch(clearTodoGroupErrors()); 25 | try { 26 | await updateAPI(requestData, TODO_GROUP_URL); 27 | 28 | debounce(() => { 29 | callback(); 30 | })(); 31 | } catch (error) { 32 | dispatch({ type: SET_ERROR, value: error }); 33 | } finally { 34 | dispatch(showMainLoader()); 35 | } 36 | }; 37 | 38 | export const deleteTodoGroups = () => async (dispatch) => {}; 39 | 40 | export const clearTodoGroupErrors = () => (dispatch) => { 41 | const error = { code: 200, message: null }; 42 | dispatch({ type: SET_ERROR, value: error }); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from "./AuthAction"; 2 | export * from "./InitAction"; 3 | export * from "./TodoGroupAction"; 4 | export * from "./TodoAction"; 5 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/IconButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { doNothing } from '../../utils/utils'; 3 | 4 | export const IconButton = ({ onClick = doNothing, title = null, active = false, icon, type }) => { 5 | const button_type = `text-${type || null}`; 6 | const style = `btn btn-sm btn ${button_type} rounded align-middle`; 7 | 8 | if (active) { 9 | return ( 10 | 11 | {icon && } 12 | {title} 13 | 14 | ); 15 | } 16 | 17 | return ( 18 | 19 | {icon && } 20 | {title} 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/index.js: -------------------------------------------------------------------------------- 1 | export * from "./IconButton"; 2 | -------------------------------------------------------------------------------- /frontend/src/components/containers/InputFieldContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const InputFieldContainer = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/src/components/containers/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/majid-cj/django_tenant_react/e5b8f5ea5f072f710a218f83715ff1f2673ffb64/frontend/src/components/containers/index.js -------------------------------------------------------------------------------- /frontend/src/components/header/Background.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | import { HOME_SCREEN } from "../../constants"; 5 | import BgImage from "../../resources/images/bg.jpg"; 6 | 7 | export const Background = (props) => { 8 | const location = useLocation(); 9 | 10 | const BackgroundWrapper = styled.div` 11 | background-image: url("${BgImage}"); 12 | background-repeat: no-repeat; 13 | background-size: cover; 14 | height: 400px; 15 | `; 16 | 17 | const BreifContainer = styled.div` 18 | width: 100%; 19 | height: 100%; 20 | background: grey; 21 | opacity: 0.75; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | `; 26 | 27 | if (location.pathname !== HOME_SCREEN) return null; 28 | 29 | return ( 30 | 31 | 32 |
33 |

Django Tenant React

34 |
35 |

36 | is a simple todo app based on multi-tenant database schema 37 |

38 |
39 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /frontend/src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useHistory } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { HOME_SCREEN } from '../../constants'; 5 | import NavLink from './NavLink'; 6 | import { Background } from './Background'; 7 | import { signoutUser } from '../../actions'; 8 | 9 | export const Header = () => { 10 | const { replace } = useHistory(); 11 | const dispatch = useDispatch(); 12 | const logged_in = useSelector((state) => state.config.logged_in); 13 | 14 | const logout = () => { 15 | dispatch( 16 | signoutUser(() => { 17 | replace({ pathname: HOME_SCREEN }); 18 | }) 19 | ); 20 | }; 21 | 22 | return ( 23 |
24 | 57 | 58 | 59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /frontend/src/components/header/NavLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ link, icon }) => ( 4 |
  • 5 | 6 | 7 | 8 |
  • 9 | ); 10 | -------------------------------------------------------------------------------- /frontend/src/components/header/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Header"; 2 | export * from "./NavLink"; 3 | export * from "./Background"; 4 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/BodyField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TextAreaField } from './TextAreaField'; 3 | 4 | export const BodyField = ({ onValue }) => { 5 | const [body, setBody] = useState(''); 6 | const [error, setError] = useState(false); 7 | 8 | const onValueChange = ({ target: { value } }) => { 9 | setBody(value); 10 | onValue(value); 11 | setError(!value.length === 0); 12 | }; 13 | 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/ConfirmPasswordField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { matchPassword } from "../../utils/validators"; 3 | import { InputField } from "./InputField"; 4 | 5 | export const ConfirmPasswordField = ({ onValue, password }) => { 6 | const [confirm_password, setConfirmPassword] = useState(""); 7 | const [error, setError] = useState(false); 8 | const [show, setShow] = useState(false); 9 | 10 | const onValueChange = ({ target: { value } }) => { 11 | setConfirmPassword(value); 12 | onValue(value); 13 | setError(!matchPassword(password.password, value)); 14 | }; 15 | 16 | const showPassword = () => setShow(!show); 17 | 18 | return ( 19 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/GroupField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { InputField } from './InputField'; 3 | 4 | export const GroupField = ({ onValue, initialValue = '' }) => { 5 | const [group, setGroup] = useState(initialValue); 6 | const [error, setError] = useState(false); 7 | 8 | const onValueChange = ({ target: { value } }) => { 9 | setGroup(value); 10 | onValue(value); 11 | setError(!value.length === 5); 12 | }; 13 | 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/InputField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { doNothing } from "../../utils/utils"; 3 | 4 | export const InputField = ({ 5 | value, 6 | onValueChange, 7 | placeholder, 8 | icon = null, 9 | type = "text", 10 | error = false, 11 | fixed = false, 12 | show, 13 | required, 14 | onClick = doNothing, 15 | }) => { 16 | const color = error ? "danger" : value ? "success" : "dark"; 17 | return ( 18 |
    19 | {icon && ( 20 | 24 | )} 25 |
    26 | 35 |
    36 |
    37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/NameField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { validateName } from "../../utils/validators"; 3 | import { InputField } from "./InputField"; 4 | 5 | export const NameField = ({ onValue }) => { 6 | const [name, setName] = useState(""); 7 | const [error, setError] = useState(false); 8 | 9 | const onValueChange = ({ target: { value } }) => { 10 | setName(value); 11 | onValue(value); 12 | setError(!validateName(value)); 13 | }; 14 | 15 | return ( 16 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/PasswordField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { validatePassword } from "../../utils/validators"; 3 | import { InputField } from "./InputField"; 4 | 5 | export const PasswordField = ({ onValue }) => { 6 | const [password, setPassword] = useState(""); 7 | const [error, setError] = useState(false); 8 | const [show, setShow] = useState(false); 9 | 10 | const onValueChange = ({ target: { value } }) => { 11 | setPassword(value); 12 | onValue(value); 13 | setError(!validatePassword(value)); 14 | }; 15 | 16 | const showPassword = () => setShow(!show); 17 | 18 | return ( 19 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/SearchField.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { InputField } from "./InputField"; 3 | 4 | export const SearchField = ({ onValue }) => { 5 | const [search, setSearch] = useState(""); 6 | 7 | const onValueChange = ({ target: { value } }) => { 8 | setSearch(value); 9 | onValue(value); 10 | }; 11 | 12 | return ( 13 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/inputs/TextAreaField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { doNothing } from '../../utils/utils'; 3 | 4 | export const TextAreaField = ({ 5 | value, 6 | onValueChange, 7 | placeholder, 8 | icon = null, 9 | type = 'text', 10 | error = false, 11 | fixed = false, 12 | show, 13 | required, 14 | onClick = doNothing, 15 | }) => { 16 | const color = error ? 'danger' : value ? 'success' : 'dark'; 17 | return ( 18 |
    19 | {icon && } 20 |
    21 |