├── .gitignore ├── BackendAPI ├── BackendAPI │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── customuser │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── constants.py │ ├── email.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200514_1809.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── drf_jwt │ ├── __init__.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── utils.py │ └── views.py ├── manage.py └── templates │ └── email │ ├── activation.html │ ├── confirmation.html │ ├── password_changed_confirmation.html │ └── password_reset.html ├── LICENSE ├── README.md ├── requirements-dev.txt └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env 3 | env 4 | venv 5 | __pycache__/ 6 | *.__pycache__/ 7 | *.pyc 8 | *.sqlite3 9 | .idea 10 | mydata 11 | media 12 | debug.log 13 | logs 14 | images 15 | static 16 | .DS_Store -------------------------------------------------------------------------------- /BackendAPI/BackendAPI/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvioramalho/django-startup-rest-api/924e4155f66b8f09b04281ec7040b1d093cdeecc/BackendAPI/BackendAPI/__init__.py -------------------------------------------------------------------------------- /BackendAPI/BackendAPI/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for BackendAPI project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | import datetime 15 | from decouple import config 16 | 17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = config('SECRET_KEY') 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = config('DEBUG', default=False, cast=bool) 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | INTERNAL_IPS = [ 33 | '127.0.0.1' 34 | ] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'drf_yasg', 47 | 'corsheaders', 48 | 'rest_framework', 49 | 'djoser', 50 | 'customuser', 51 | 52 | 53 | # Sempre por ultimo 54 | 'debug_toolbar', 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | 'django.middleware.security.SecurityMiddleware', 59 | 'django.contrib.sessions.middleware.SessionMiddleware', 60 | 'corsheaders.middleware.CorsMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.messages.middleware.MessageMiddleware', 65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 66 | 67 | # Sempre por ultimo 68 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 69 | ] 70 | 71 | ROOT_URLCONF = 'BackendAPI.urls' 72 | 73 | TEMPLATES = [ 74 | { 75 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 76 | 'DIRS': [ 77 | BASE_DIR, os.path.join(BASE_DIR, 'templates') 78 | ], 79 | 'APP_DIRS': True, 80 | 'OPTIONS': { 81 | 'context_processors': [ 82 | 'django.template.context_processors.debug', 83 | 'django.template.context_processors.request', 84 | 'django.contrib.auth.context_processors.auth', 85 | 'django.contrib.messages.context_processors.messages', 86 | ], 87 | }, 88 | }, 89 | ] 90 | 91 | WSGI_APPLICATION = 'BackendAPI.wsgi.application' 92 | 93 | 94 | # Database 95 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 96 | 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 101 | } 102 | } 103 | 104 | 105 | # Password validation 106 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 107 | 108 | AUTH_PASSWORD_VALIDATORS = [ 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 117 | }, 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 120 | }, 121 | ] 122 | 123 | 124 | # Internationalization 125 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 126 | 127 | LANGUAGE_CODE = 'pt-BR' 128 | 129 | TIME_ZONE = 'America/Sao_Paulo' 130 | 131 | USE_I18N = True 132 | 133 | USE_L10N = True 134 | 135 | USE_TZ = True 136 | 137 | 138 | # Static files (CSS, JavaScript, Images) 139 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 140 | 141 | STATIC_URL = '/static/' 142 | 143 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 144 | 145 | MEDIA_ROOT = 'images' 146 | 147 | MEDIA_URL = '/media/' 148 | 149 | FRONTEND_URL = config('FRONTEND_URL') 150 | 151 | CORS_ORIGIN_ALLOW_ALL = True 152 | 153 | AUTH_USER_MODEL = 'customuser.CustomUser' 154 | 155 | REST_FRAMEWORK = { 156 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 157 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 158 | ), 159 | } 160 | 161 | JWT_AUTH = { 162 | 'JWT_AUTH_HEADER_PREFIX': 'Bearer', 163 | 'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=15), 164 | 'JWT_GET_USER_SECRET_KEY': 'customuser.models.jwt_get_secret_key', 165 | 'JWT_PAYLOAD_HANDLER': 'customuser.utils.jwt_otp_payload', 166 | 'JWT_ALLOW_REFRESH': True, 167 | 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 168 | 'JWT_DECODE_HANDLER': 'drf_jwt.utils.jwt_decode_handler', 169 | } 170 | 171 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 172 | EMAIL_HOST = config('EMAIL_HOST') 173 | EMAIL_HOST_USER = config('EMAIL_HOST_USER') 174 | EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') 175 | EMAIL_PORT=config('EMAIL_PORT', cast=int) 176 | EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool) 177 | DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL') 178 | 179 | DJOSER = { 180 | 'USER_CREATE_PASSWORD_RETYPE': True, 181 | 'ACTIVATION_URL': '#/activate/{uid}/{token}', 182 | 'SEND_ACTIVATION_EMAIL': True, 183 | 'SEND_CONFIRMATION_EMAIL': True, 184 | 'SERIALIZERS': { 185 | 'activation': 'customuser.serializers.CustomSerializer', 186 | }, 187 | 'CONSTANTS': { 188 | 'messages': 'customuser.constants.CustomMessages', 189 | }, 190 | 'PASSWORD_RESET_CONFIRM_URL': '#/reset-password/{uid}/{token}', 191 | 'SET_PASSWORD_RETYPE': True, 192 | 'EMAIL': { 193 | 'activation': 'customuser.email.CustomActivationEmail', 194 | 'confirmation': 'customuser.email.CustomConfirmationEmail', 195 | 'password_changed_confirmation': 'customuser.email.CustomPasswordChangedConfirmationEmail', 196 | 'password_reset': 'customuser.email.CustomPasswordResetEmail', 197 | } 198 | } -------------------------------------------------------------------------------- /BackendAPI/BackendAPI/urls.py: -------------------------------------------------------------------------------- 1 | """BackendAPI URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/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, re_path 18 | from django.conf.urls.static import static 19 | from django.conf import settings 20 | from rest_framework import permissions 21 | from drf_yasg.views import get_schema_view 22 | from drf_yasg import openapi 23 | 24 | schema_view = get_schema_view( 25 | openapi.Info( 26 | title="Backend API", 27 | default_version='v1', 28 | description="Test description", 29 | terms_of_service="https://www.google.com/policies/terms/", 30 | contact=openapi.Contact(email="contact@snippets.local"), 31 | license=openapi.License(name="BSD License"), 32 | ), 33 | public=True, 34 | permission_classes=(permissions.AllowAny,), 35 | ) 36 | 37 | urlpatterns = [ 38 | path('admin/', admin.site.urls), 39 | re_path(r'^api/', include('customuser.urls')), 40 | ## If you want to use SWAGGER you can uncomment the lines below 41 | # path('swagger/', schema_view.without_ui(cache_timeout=0), name='schema-json'), 42 | # path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), 43 | ## If you want to use REDOC you can uncomment the line below 44 | path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), 45 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 46 | 47 | if settings.DEBUG: 48 | import debug_toolbar 49 | urlpatterns = [ 50 | path('__debug__/', include(debug_toolbar.urls)), 51 | ] + urlpatterns 52 | -------------------------------------------------------------------------------- /BackendAPI/BackendAPI/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for BackendAPI 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/2.2/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', 'BackendAPI.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /BackendAPI/customuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvioramalho/django-startup-rest-api/924e4155f66b8f09b04281ec7040b1d093cdeecc/BackendAPI/customuser/__init__.py -------------------------------------------------------------------------------- /BackendAPI/customuser/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.admin.actions import delete_selected 3 | 4 | from .models import CustomUser 5 | from djoser import signals, utils 6 | from djoser.compat import get_user_email 7 | from djoser.conf import settings 8 | 9 | def make_activate(modeladmin, request, queryset): 10 | queryset.update(is_active=True) 11 | 12 | def block_user(modeladmin, request, queryset): 13 | queryset.update(is_blocked=True) 14 | 15 | def send_confirmation_email(modeladmin, request, queryset): 16 | 17 | for user in queryset: 18 | if settings.SEND_CONFIRMATION_EMAIL: 19 | context = {"user": user} 20 | to = [get_user_email(user)] 21 | settings.EMAIL.confirmation(request, context).send(to) 22 | 23 | def send_resend_activation_email(modeladmin, request, queryset): 24 | 25 | for user in queryset: 26 | if settings.SEND_CONFIRMATION_EMAIL: 27 | context = {"user": user} 28 | to = [get_user_email(user)] 29 | settings.EMAIL.activation(request, context).send(to) 30 | 31 | 32 | def send_reset_password_email(modeladmin, request, queryset): 33 | 34 | for user in queryset: 35 | if settings.SEND_CONFIRMATION_EMAIL: 36 | context = {"user": user} 37 | to = [get_user_email(user)] 38 | settings.EMAIL.password_reset(request, context).send(to) 39 | 40 | 41 | make_activate.short_description = "Ativar Usuarios" 42 | send_confirmation_email.short_description = "Enviar Email de Confirmacao" 43 | send_resend_activation_email.short_description = "Reenviar Email de Ativação" 44 | send_reset_password_email.short_description = "Enviar link para Troca de Senha" 45 | block_user.short_description = "Bloquear Usuário" 46 | 47 | # Exclui globalmente a ação de apagar selecionados 48 | # É necessário adicionar apenas nos que quiser conforme abaixo 49 | admin.site.disable_action('delete_selected') 50 | delete_selected.short_description = "Apagar Selecionados" 51 | 52 | class CustomUserAdmin(admin.ModelAdmin): 53 | list_display = ('email', 'is_active', 'is_blocked', 'is_admin', 'is_staff') 54 | fields = [('email', 'is_active', 'is_blocked', 'is_admin', 'is_staff')] 55 | list_filter = ['is_active', 'is_blocked', 'is_admin', 'is_staff'] 56 | search_fields = ['email'] 57 | actions = [ 58 | make_activate, send_confirmation_email, block_user, 59 | send_resend_activation_email, send_reset_password_email, 60 | 'delete_selected'] 61 | 62 | admin.site.site_header = 'Titulo do Topo' 63 | admin.site.index_title = 'Titulo do Index' 64 | admin.site.site_title = 'Titulo do Site(HTML)' 65 | 66 | admin.site.register(CustomUser, CustomUserAdmin) 67 | -------------------------------------------------------------------------------- /BackendAPI/customuser/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CustomuserConfig(AppConfig): 5 | name = 'customuser' 6 | -------------------------------------------------------------------------------- /BackendAPI/customuser/constants.py: -------------------------------------------------------------------------------- 1 | from djoser.constants import Messages 2 | 3 | 4 | class CustomMessages(Messages): 5 | INVALID_CREDENTIALS_ERROR = ("Não foi possível efetuar login com credenciais fornecidas.") 6 | INACTIVE_ACCOUNT_ERROR = ("A conta de usuário está desativada.") 7 | INVALID_TOKEN_ERROR = ("Token inválido para este usuário.") 8 | INVALID_UID_ERROR = ("ID de usuário inválido ou usuário não existe.") 9 | STALE_TOKEN_ERROR = ("Token expirado para este usuário.") 10 | PASSWORD_MISMATCH_ERROR = ("Os dois campos de senha não coincidem.") 11 | USERNAME_MISMATCH_ERROR = ("Os dois campos {0} não coincidem.") 12 | INVALID_PASSWORD_ERROR = ("Senha inválida.") 13 | EMAIL_NOT_FOUND = ("Este usuário não existe.") 14 | CANNOT_CREATE_USER_ERROR = ("Não foi possível criar a conta.") 15 | BLOCKED_ERROR = ("Usuário bloqueado.") -------------------------------------------------------------------------------- /BackendAPI/customuser/email.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from djoser.email import PasswordResetEmail, ConfirmationEmail, ActivationEmail, PasswordChangedConfirmationEmail 3 | 4 | 5 | class CustomActivationEmail(ActivationEmail): 6 | template_name = "email/activation.html" 7 | 8 | def get_context_data(self): 9 | context = super().get_context_data() 10 | context["frontend"] = settings.FRONTEND_URL 11 | return context 12 | 13 | class CustomConfirmationEmail(ConfirmationEmail): 14 | template_name = "email/confirmation.html" 15 | 16 | class CustomPasswordChangedConfirmationEmail(PasswordChangedConfirmationEmail): 17 | template_name = "email/password_changed_confirmation.html" 18 | 19 | class CustomPasswordResetEmail(PasswordResetEmail): 20 | template_name = "email/password_reset.html" 21 | 22 | def get_context_data(self): 23 | context = super().get_context_data() 24 | context["frontend"] = settings.FRONTEND_URL 25 | return context -------------------------------------------------------------------------------- /BackendAPI/customuser/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2020-01-02 23:10 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='CustomUser', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('password', models.CharField(max_length=128, verbose_name='password')), 20 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 21 | ('email', models.EmailField(db_column='usu_des_email', max_length=255, unique=True, verbose_name='e-mail')), 22 | ('is_active', models.BooleanField(db_column='usu_idc_ativo', default=True, verbose_name='ativo')), 23 | ('is_admin', models.BooleanField(db_column='usu_idc_admin', default=False, verbose_name='admin')), 24 | ('jwt_secret', models.UUIDField(db_column='usu_uid_jwtsecret', default=uuid.uuid4, verbose_name='JWT Secret')), 25 | ('is_blocked', models.BooleanField(db_column='usu_idc_bloqueado', default=False, verbose_name='bloqueado')), 26 | ('is_staff', models.BooleanField(db_column='usu_idc_staff', default=False, verbose_name='is staff')), 27 | ('phone', models.CharField(blank=True, db_column='usu_des_telefone', max_length=11, null=True, verbose_name='telefone')), 28 | ('photo', models.ImageField(blank=True, db_column='usu_img_foto', null=True, upload_to='photos', verbose_name='foto')), 29 | ], 30 | options={ 31 | 'db_table': 'usuario_usu', 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /BackendAPI/customuser/migrations/0002_auto_20200514_1809.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.7 on 2020-05-14 21:09 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('customuser', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='customuser', 15 | options={'verbose_name': 'usuário'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /BackendAPI/customuser/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvioramalho/django-startup-rest-api/924e4155f66b8f09b04281ec7040b1d093cdeecc/BackendAPI/customuser/migrations/__init__.py -------------------------------------------------------------------------------- /BackendAPI/customuser/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import uuid 3 | from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager 4 | import datetime 5 | 6 | class CustomUserManager(BaseUserManager): 7 | 8 | def create_user(self, email, password=None): 9 | ''' 10 | Cria e salva um usuário com o email e a senha fornecidos. 11 | ''' 12 | if not email: 13 | raise ValueError('Users must have an email address') 14 | user = self.model( 15 | email=self.normalize_email(email), 16 | ) 17 | user.set_password(password) 18 | user.save(using=self._db) 19 | return user 20 | 21 | def create_superuser(self, email, password): 22 | ''' 23 | Cria e salva um superusuário com o email e a senha fornecidos. 24 | ''' 25 | user = self.create_user( 26 | email, 27 | password=password, 28 | ) 29 | user.is_admin = True 30 | user.is_staff = True 31 | user.save(using=self._db) 32 | return user 33 | 34 | class CustomUser(AbstractBaseUser): 35 | 36 | email = models.EmailField(db_column='usu_des_email', verbose_name='e-mail', max_length=255, unique=True) 37 | is_active = models.BooleanField(db_column='usu_idc_ativo', default=True, verbose_name="ativo") 38 | is_admin = models.BooleanField(db_column='usu_idc_admin', default=False, verbose_name="admin") 39 | jwt_secret = models.UUIDField(db_column='usu_uid_jwtsecret', default=uuid.uuid4, verbose_name="JWT Secret") 40 | is_blocked = models.BooleanField(db_column='usu_idc_bloqueado', default=False, verbose_name="bloqueado") 41 | is_staff = models.BooleanField(db_column='usu_idc_staff', default=False, verbose_name="is staff") 42 | phone = phone = models.CharField(db_column='usu_des_telefone', max_length=11, blank=True, null=True, verbose_name="telefone") 43 | photo = models.ImageField(db_column='usu_img_foto', upload_to='photos', null=True, blank=True, verbose_name="foto") 44 | objects = CustomUserManager() 45 | USERNAME_FIELD = 'email' 46 | 47 | class Meta: 48 | verbose_name = 'usuário' 49 | db_table = 'usuario_usu' 50 | 51 | def __str__(self): 52 | return self.email 53 | 54 | def has_module_perms(self, app_label): 55 | return True 56 | 57 | def has_perm(self, perm, obj=None): 58 | return True 59 | 60 | def jwt_get_secret_key(user_model): 61 | return user_model.jwt_secret 62 | 63 | -------------------------------------------------------------------------------- /BackendAPI/customuser/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | class IsUserBlocked(permissions.BasePermission): 4 | ''' 5 | Se o usuário estiver bloqueado. 6 | ''' 7 | message = "Seu usuário está bloqueado. Entre em contato com o administrador do sistema." 8 | 9 | def has_permission(self, request, view): 10 | print('Permission User: ', request.user.is_blocked) 11 | if request.user: 12 | return not request.user.is_blocked 13 | else: 14 | return False 15 | -------------------------------------------------------------------------------- /BackendAPI/customuser/serializers.py: -------------------------------------------------------------------------------- 1 | from djoser.serializers import ActivationSerializer 2 | from rest_framework import exceptions, serializers 3 | from rest_framework.serializers import ModelSerializer 4 | from djoser.conf import settings 5 | 6 | from rest_framework_jwt.serializers import JSONWebTokenSerializer 7 | from drf_jwt.serializers import RefreshJSONWebTokenSerializer 8 | 9 | from .models import CustomUser 10 | 11 | class CustomSerializer(ActivationSerializer): 12 | 13 | default_error_messages = { 14 | "stale_token": settings.CONSTANTS.messages.STALE_TOKEN_ERROR, 15 | "blocked": settings.CONSTANTS.messages.BLOCKED_ERROR 16 | } 17 | 18 | def validate(self, attrs): 19 | attrs = super().validate(attrs) 20 | if not self.user.is_blocked: 21 | return attrs 22 | raise exceptions.PermissionDenied(self.error_messages["blocked"]) 23 | 24 | class CustomTokenSerializer(JSONWebTokenSerializer): 25 | 26 | def validate(self, attrs): 27 | attrs = super().validate(attrs) 28 | user = attrs.get('user') 29 | if not user.is_blocked: 30 | return attrs 31 | else: 32 | raise serializers.ValidationError('Seu usuário encontra-se bloqueado no sistema. Entre em contato com o administrador do sistema.') 33 | 34 | 35 | class CustomRefreshTokenSerializer(RefreshJSONWebTokenSerializer): 36 | 37 | def validate(self, attrs): 38 | attrs = super().validate(attrs) 39 | user = attrs.get('user') 40 | if not user.is_blocked: 41 | return attrs 42 | else: 43 | raise serializers.ValidationError('Seu usuário encontra-se bloqueado no sistema. Entre em contato com o administrador do sistema.') 44 | 45 | class CustomUserSerializer(ModelSerializer): 46 | class Meta: 47 | model = CustomUser 48 | fields = ('id', 'phone') 49 | 50 | 51 | class CustomUserPhotoSerializer(ModelSerializer): 52 | class Meta: 53 | model = CustomUser 54 | fields = ('id', 'photo') -------------------------------------------------------------------------------- /BackendAPI/customuser/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /BackendAPI/customuser/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from django.urls import path 3 | from djoser import views as djoser_views 4 | from drf_jwt import views as jwt_views 5 | from .views import CustomUserView, CustomUserDeleteView, CustomUserLogin, CustomRefreshToken, CustomUserCreateView 6 | 7 | from customuser import views 8 | 9 | urlpatterns = [ 10 | # As VIEWS são definidas no Dojser, mas estamos atribuindo caminhos personalizados. 11 | re_path(r'^user/view/$', views.CustomUserView.as_view({'get': 'me'}), name='user-view'), 12 | re_path(r'^user/delete/$', views.CustomUserDeleteView.as_view({'delete': 'destroy'}), name='user-delete'), 13 | re_path(r'^user/logout/all/$', views.CustomUserLogoutAllView.as_view(), name='user-logout-all'), 14 | #re_path(r'^user/create/$', djoser_views.UserViewSet.as_view({'post': 'create'}), name='user-create'), 15 | re_path(r'^user/create/$', CustomUserCreateView.as_view({'post': 'create'}), name='user-create'), 16 | re_path(r'^user/activation/$', djoser_views.UserViewSet.as_view({'post': 'activation'}), name='user-activation'), 17 | re_path(r'^user/forget-password/$', djoser_views.UserViewSet.as_view({'post': 'reset_password'}), name='reset-password'), 18 | re_path(r'^user/reset-password-confirm/$', djoser_views.UserViewSet.as_view({'post': 'reset_password_confirm'}), name='reset-password-confirm'), 19 | re_path(r'^user/set-password/$', djoser_views.UserViewSet.as_view({'post': 'set_password'}), name='set-password'), 20 | 21 | # As VIEWS são definidas no DRF-JWT, mas estamos atribuindo caminhos personalizados. 22 | re_path(r'^user/login/$', views.CustomUserLogin.as_view(), name='user-login'), 23 | re_path(r'^user/login/refresh/$', views.CustomRefreshToken.as_view(), name='user-login-refresh'), 24 | re_path(r'^user/login/verify/$', jwt_views.verify_jwt_token, name='user-token-verify'), 25 | 26 | # As VIEWS customizadas de usuário 27 | path('user/photo//', views.CustomUserPhotoViewSet.as_view({'post': 'partial_update', 'get': 'retrieve'}), name='update-user-photo'), 28 | path('user/data//', views.CustomUserViewSet.as_view({'get': 'retrieve', 'put': 'partial_update'}), name='user-data'), 29 | ] -------------------------------------------------------------------------------- /BackendAPI/customuser/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from calendar import timegm 3 | 4 | from rest_framework_jwt.compat import get_username, get_username_field 5 | from rest_framework_jwt.settings import api_settings 6 | 7 | 8 | def jwt_otp_payload(user, device = None): 9 | """ 10 | Opcionalmente inclui o Device TOP no payload do JWT 11 | """ 12 | username_field = get_username_field() 13 | username = get_username(user) 14 | 15 | payload = { 16 | 'user_id': user.pk, 17 | 'username': username, 18 | 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA 19 | } 20 | 21 | # Include original issued at time for a brand new token, 22 | # to allow token refresh 23 | if api_settings.JWT_ALLOW_REFRESH: 24 | payload['orig_iat'] = timegm( 25 | datetime.utcnow().utctimetuple() 26 | ) 27 | 28 | if api_settings.JWT_AUDIENCE is not None: 29 | payload['aud'] = api_settings.JWT_AUDIENCE 30 | 31 | if api_settings.JWT_ISSUER is not None: 32 | payload['iss'] = api_settings.JWT_ISSUER 33 | 34 | return payload -------------------------------------------------------------------------------- /BackendAPI/customuser/views.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from rest_framework import views, permissions, status 4 | from rest_framework.viewsets import ModelViewSet 5 | from rest_framework.response import Response 6 | 7 | from datetime import datetime, timedelta 8 | from calendar import timegm 9 | from djoser.views import UserViewSet 10 | from djoser import serializers 11 | from rest_framework import permissions 12 | 13 | from customuser.models import CustomUser 14 | from customuser import permissions as custom_permissions 15 | from .models import CustomUser 16 | from .serializers import CustomUserPhotoSerializer, CustomUserSerializer 17 | 18 | from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken 19 | from .serializers import CustomTokenSerializer, CustomRefreshTokenSerializer 20 | from drf_jwt.utils import jwt_decode_handler 21 | 22 | 23 | class CustomUserLogoutAllView(views.APIView): 24 | ''' 25 | Use este endpoint para efetuar logout de todas as sessões para um determinado usuário. 26 | ''' 27 | permission_classes = [permissions.IsAuthenticated] 28 | def post(self, request, *args, **kwargs): 29 | user = request.user 30 | user.jwt_secret = uuid.uuid4() 31 | user.save() 32 | return Response(status=status.HTTP_204_NO_CONTENT) 33 | 34 | 35 | class CustomUserCreateView(UserViewSet): 36 | ''' 37 | Uses the default Djoser view, but add custom fields (phone in this case) 38 | Use this endpoint to create user. 39 | ''' 40 | model = CustomUser 41 | serializer_class = serializers.UserCreateSerializer 42 | 43 | def create(self, request, *args, **kwargs): 44 | response = super().create(request, *args, **kwargs) 45 | user = CustomUser.objects.get(pk=response.data.get('id')) 46 | user.phone = request.data.get('phone') 47 | user.save() 48 | return response 49 | 50 | 51 | class CustomUserView(UserViewSet): 52 | ''' 53 | Uses the default Djoser view, but add the IsOtpVerified permission. 54 | Use this endpoint to retrieve/update user. 55 | ''' 56 | model = CustomUser 57 | serializer_class = serializers.UserSerializer 58 | permission_classes = [permissions.IsAuthenticated, custom_permissions.IsUserBlocked] 59 | 60 | def me(self, request, *args, **kwargs): 61 | return super().me(request, *args, **kwargs) 62 | 63 | class CustomUserDeleteView(UserViewSet): 64 | ''' 65 | Uses the default Djoser view, but add the IsOtpVerified permission. 66 | Use this endpoint to remove actually authenticated user. 67 | ''' 68 | serializer_class = serializers.UserDeleteSerializer 69 | permission_classes = [permissions.IsAuthenticated] 70 | 71 | class CustomUserLogin(ObtainJSONWebToken): 72 | 73 | serializer_class = CustomTokenSerializer 74 | 75 | def post(self, request, *args, **kwargs): 76 | response = super(CustomUserLogin, self).post(request, *args, **kwargs) 77 | try: 78 | user_id = jwt_decode_handler(response.data.get('token')).get('user_id') 79 | user = CustomUser.objects.get(pk=user_id) 80 | if user.photo: 81 | response.data['photo'] = request.META['wsgi.url_scheme'] + '://' + request.META['HTTP_HOST'] + user.photo.url 82 | except: 83 | pass 84 | return response 85 | 86 | class CustomRefreshToken(RefreshJSONWebToken): 87 | 88 | serializer_class = CustomRefreshTokenSerializer 89 | 90 | class CustomUserPhotoViewSet(ModelViewSet): 91 | serializer_class = CustomUserPhotoSerializer 92 | permission_classes = [permissions.IsAuthenticated] 93 | lookup_field = 'id' 94 | 95 | def get_queryset(self): 96 | id = self.request.user.id # self.request.query_params.get('id', None) 97 | queryset = None 98 | 99 | if id: 100 | queryset = CustomUser.objects.filter(pk=id) 101 | else: 102 | queryset = CustomUser.objects.all() 103 | 104 | return queryset 105 | 106 | def retrieve(self, request, *args, **kwargs): 107 | return super(CustomUserPhotoViewSet, self).retrieve(request, *args, **kwargs) 108 | 109 | def partial_update(self, request, *args, **kwargs): 110 | return super(CustomUserPhotoViewSet, self).partial_update(request, *args, **kwargs) 111 | 112 | class CustomUserViewSet(ModelViewSet): 113 | 114 | serializer_class = CustomUserSerializer 115 | permission_classes = [permissions.IsAuthenticated] 116 | lookup_field = 'id' 117 | 118 | def get_queryset(self): 119 | id = self.request.user.id # self.request.query_params.get('id', None) 120 | queryset = None 121 | 122 | if id: 123 | queryset = CustomUser.objects.filter(pk=id) 124 | else: 125 | queryset = CustomUser.objects.all() 126 | 127 | return queryset 128 | 129 | def retrieve(self, request, *args, **kwargs): 130 | return super(CustomUserViewSet, self).retrieve(request, *args, **kwargs) 131 | 132 | def post(self, request, *args, **kwargs): 133 | return super(CustomUserViewSet, self).partial_update(request, *args, **kwargs) 134 | -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvioramalho/django-startup-rest-api/924e4155f66b8f09b04281ec7040b1d093cdeecc/BackendAPI/drf_jwt/__init__.py -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DrfJwtConfig(AppConfig): 5 | name = 'drf_jwt' 6 | -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvioramalho/django-startup-rest-api/924e4155f66b8f09b04281ec7040b1d093cdeecc/BackendAPI/drf_jwt/migrations/__init__.py -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/serializers.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from calendar import timegm 3 | from datetime import datetime, timedelta 4 | 5 | from django.contrib.auth import get_user_model 6 | from rest_framework_jwt.compat import Serializer 7 | from rest_framework import serializers 8 | from rest_framework_jwt.settings import api_settings 9 | 10 | User = get_user_model() 11 | jwt_decode_handler = api_settings.JWT_DECODE_HANDLER 12 | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 13 | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 14 | jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER 15 | 16 | 17 | class VerificationBaseSerializer(Serializer): 18 | """ 19 | Abstract serializer used for verifying and refreshing JWTs. 20 | """ 21 | token = serializers.CharField() 22 | 23 | def validate(self, attrs): 24 | msg = 'Please define a validate method.' 25 | raise NotImplementedError(msg) 26 | 27 | def _check_payload(self, token, isRefresh=False): 28 | # Check payload valid (based off of JSONWebTokenAuthentication, 29 | # may want to refactor) 30 | try: 31 | payload = jwt_decode_handler(token, isRefresh) 32 | except jwt.ExpiredSignatureError: 33 | msg = ('Signature has expired.') 34 | raise serializers.ValidationError(msg) 35 | except jwt.DecodeError: 36 | msg = ('Error decoding signature.') 37 | raise serializers.ValidationError(msg) 38 | 39 | return payload 40 | 41 | def _check_user(self, payload): 42 | username = jwt_get_username_from_payload(payload) 43 | 44 | if not username: 45 | msg = ('Invalid payload.') 46 | raise serializers.ValidationError(msg) 47 | 48 | # Make sure user exists 49 | try: 50 | user = User.objects.get_by_natural_key(username) 51 | except User.DoesNotExist: 52 | msg = ("User doesn't exist.") 53 | raise serializers.ValidationError(msg) 54 | 55 | if not user.is_active: 56 | msg = ('User account is disabled.') 57 | raise serializers.ValidationError(msg) 58 | 59 | return user 60 | 61 | class VerifyJSONWebTokenSerializer(VerificationBaseSerializer): 62 | """ 63 | Check the veracity of an access token. 64 | """ 65 | 66 | def validate(self, attrs): 67 | token = attrs['token'] 68 | 69 | payload = self._check_payload(token=token) 70 | user = self._check_user(payload=payload) 71 | 72 | return { 73 | 'token': token, 74 | 'user': user 75 | } 76 | 77 | class RefreshJSONWebTokenSerializer(VerificationBaseSerializer): 78 | """ 79 | Refresh an access token. 80 | """ 81 | 82 | def validate(self, attrs): 83 | token = attrs['token'] 84 | payload = self._check_payload(token=token, isRefresh=True) 85 | user = self._check_user(payload=payload) 86 | 87 | # Get and check 'orig_iat' 88 | orig_iat = payload.get('orig_iat') 89 | if orig_iat: 90 | # Verify expiration 91 | refresh_limit = api_settings.JWT_REFRESH_EXPIRATION_DELTA 92 | 93 | if isinstance(refresh_limit, timedelta): 94 | refresh_limit = (refresh_limit.days * 24 * 3600 + 95 | refresh_limit.seconds) 96 | 97 | expiration_timestamp = orig_iat + int(refresh_limit) 98 | now_timestamp = timegm(datetime.utcnow().utctimetuple()) 99 | 100 | if now_timestamp > expiration_timestamp: 101 | msg = ('Refresh has expired.') 102 | raise serializers.ValidationError(msg) 103 | else: 104 | msg = ('orig_iat field is required.') 105 | raise serializers.ValidationError(msg) 106 | 107 | new_payload = jwt_payload_handler(user) 108 | new_payload['orig_iat'] = orig_iat 109 | 110 | return { 111 | 'token': jwt_encode_handler(new_payload), 112 | 'user': user 113 | } -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/utils.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from rest_framework_jwt.settings import api_settings 3 | from rest_framework_jwt.utils import jwt_get_secret_key 4 | 5 | def jwt_decode_handler(token, isRefresh=False): 6 | options = { 7 | 'verify_exp': api_settings.JWT_VERIFY_EXPIRATION, 8 | } 9 | if isRefresh: 10 | options = { 11 | 'verify_exp': False, 12 | } 13 | 14 | unverified_payload = jwt.decode(token, options={"verify_signature": False}) 15 | secret_key = jwt_get_secret_key(unverified_payload) 16 | return jwt.decode( 17 | token, 18 | key=api_settings.JWT_PUBLIC_KEY or secret_key, 19 | verify=api_settings.JWT_VERIFY, 20 | options=options, 21 | leeway=api_settings.JWT_LEEWAY, 22 | audience=api_settings.JWT_AUDIENCE, 23 | issuer=api_settings.JWT_ISSUER, 24 | algorithms=[api_settings.JWT_ALGORITHM] 25 | ) -------------------------------------------------------------------------------- /BackendAPI/drf_jwt/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from rest_framework_jwt.views import JSONWebTokenAPIView 3 | from rest_framework_jwt.serializers import JSONWebTokenSerializer 4 | 5 | from .serializers import VerifyJSONWebTokenSerializer, RefreshJSONWebTokenSerializer 6 | 7 | class ObtainJSONWebToken(JSONWebTokenAPIView): 8 | 9 | serializer_class = JSONWebTokenSerializer 10 | 11 | class VerifyJSONWebToken(JSONWebTokenAPIView): 12 | 13 | serializer_class = VerifyJSONWebTokenSerializer 14 | 15 | class RefreshJSONWebToken(JSONWebTokenAPIView): 16 | 17 | serializer_class = RefreshJSONWebTokenSerializer 18 | 19 | obtain_jwt_token = ObtainJSONWebToken.as_view() 20 | refresh_jwt_token = RefreshJSONWebToken.as_view() 21 | verify_jwt_token = VerifyJSONWebToken.as_view() -------------------------------------------------------------------------------- /BackendAPI/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BackendAPI.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /BackendAPI/templates/email/activation.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% block subject %} 4 | {% blocktrans %}Ativação da conta em {{ site_name }}{% endblocktrans %} 5 | {% endblock subject %} 6 | 7 | {% block text_body %} 8 | {% blocktrans %}Você está recebendo este e-mail porque precisa concluir o processo de ativação em {{ site_name }}.{% endblocktrans %} 9 | 10 | {% trans "Acesse a página abaixo para ativar a conta:" %} 11 | {{ protocol }}://{{ frontend }}/{{ url|safe }} 12 | 13 | {% trans "Obrigado por usar o nosso site!" %} 14 | 15 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 16 | {% endblock text_body %} 17 | 18 | {% block html_body %} 19 |

{% blocktrans %}Você está recebendo este e-mail porque precisa concluir o processo de ativação em {{ site_name }}.{% endblocktrans %}

20 | 21 |

{% trans "Acesse a página abaixo para ativar a conta:" %}

22 |

{{ protocol }}://{{ frontend }}/{{ url|safe }}

23 | 24 |

{% trans "Obrigado por usar o nosso site!" %}

25 | 26 |

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

27 | 28 | {% endblock html_body %} 29 | -------------------------------------------------------------------------------- /BackendAPI/templates/email/confirmation.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% block subject %} 4 | {% blocktrans %}{{ site_name }} - Sua conta foi criada e ativada com sucesso!{% endblocktrans %} 5 | {% endblock %} 6 | 7 | {% block text_body %} 8 | {% trans "Sua conta foi criada e está pronta para uso!" %} 9 | 10 | {% trans "Obrigado por usar o nosso site!" %} 11 | 12 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 13 | {% endblock text_body %} 14 | 15 | {% block html_body %} 16 |

{% trans "Sua conta foi criada e está pronta para uso!" %}

17 | 18 |

{% trans "Obrigado por usar o nosso site!" %}

19 | 20 |

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

21 | {% endblock html_body %} 22 | -------------------------------------------------------------------------------- /BackendAPI/templates/email/password_changed_confirmation.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% block subject %} 4 | {% blocktrans %}{{ site_name }} - Sua senha foi alterada com sucesso!{% endblocktrans %} 5 | {% endblock %} 6 | 7 | {% block text_body %} 8 | {% trans "Sua senha foi mudada!" %} 9 | 10 | {% trans "Obrigado por usar o nosso site!" %} 11 | 12 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 13 | {% endblock text_body %} 14 | 15 | {% block html_body %} 16 |

{% trans "Sua senha foi mudada!" %}

17 | 18 |

{% trans "Obrigado por usar o nosso site!" %}

19 | 20 |

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

21 | {% endblock html_body %} 22 | -------------------------------------------------------------------------------- /BackendAPI/templates/email/password_reset.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% block subject %} 4 | {% blocktrans %}Esqueci minha senha em {{ site_name }}{% endblocktrans %} 5 | {% endblock subject %} 6 | 7 | {% block text_body %} 8 | {% blocktrans %}Você recebeu esse e-mail por ter solicitado o reset da sua senha em {{ site_name }}.{% endblocktrans %} 9 | 10 | {% trans "Por favor cole o link abaixo em um navegador e informe uma nova senha" %} 11 | {{ protocol }}://{{ frontend }}/{{ url }} 12 | {% trans "Caso tenha esquecido, seu usuário é:" %} {{ user.get_username }} 13 | 14 | {% trans "Obrigado por usar nosso site!" %} 15 | 16 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 17 | {% endblock text_body %} 18 | 19 | {% block html_body %} 20 |

{% blocktrans %}Você recebeu esse e-mail por ter solicitado o reset da sua senha em {{ site_name }}.{% endblocktrans %}

21 | 22 |

{% trans "Por favor clique no link abaixo e informe uma nova senha:" %}

23 | {{ protocol }}://{{ frontend }}/{{ url }} 24 |

{% trans "Caso tenha esquecido, seu usuário é:" %} {{ user.get_username }}

25 | 26 |

{% trans "Obrigado por usar nosso site!" %}

27 | 28 |

{% blocktrans %}The {{ site_name }} team{% endblocktrans %}

29 | {% endblock html_body %} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Silvio Ramalho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django API - Startup Project 2 | 3 | Using DJOSER + DJANGO REST FRAMEWORK JWT 4 | 5 | I decided to create this boilerplate to facilitate the implementation of new solutions, where in most cases it is necessary to provide the features that this project has. 6 | 7 | 8 | ## Features 9 | 10 | - REST API with JWT 11 | - Send activation email on register (REST API) 12 | - Login with email 13 | - Activate multiple users 14 | - Send confirmation email 15 | - Block user 16 | - Resend activation email 17 | - Send link to change password (Frontend necessary) 18 | - Application that fix a `rest_framework_jwt` issue 19 | 20 | 21 | ## Development Install 22 | 23 | ### Clone Repo 24 | 25 | ``` 26 | git clone git@github.com:silvioramalho/django-boilerplate-api.git 27 | ``` 28 | 29 | ### Setup env 30 | 31 | ``` 32 | cd 33 | python3 -m venv env 34 | . env/bin/activate 35 | ``` 36 | 37 | ### Upgrade pip 38 | 39 | ``` 40 | pip install --upgrade pip 41 | ``` 42 | 43 | ### Install Dependencies (dev) 44 | 45 | ``` 46 | pip install -r requirements-dev.txt 47 | ``` 48 | ### Creating .env file 49 | 50 | Create an .env file at the root path and insert the following variables 51 | 52 | ``` 53 | SECRET_KEY=xxxxyyyyttttttxxxxyyyyyyxxxxxxxoooooiiii 54 | DEBUG=True 55 | 56 | FRONTEND_URL=localhost:4200 57 | 58 | EMAIL_HOST=smtp.gmail.com 59 | EMAIL_HOST_USER=your-email@gmail.com 60 | EMAIL_HOST_PASSWORD=google-app-password 61 | EMAIL_PORT=587 62 | EMAIL_USE_TLS =True 63 | DEFAULT_FROM_EMAIL=your-email@gmail.com 64 | ``` 65 | 66 | `Note: Without spaces and single quotes` 67 | 68 | ### Collects the static files 69 | 70 | ``` 71 | cd BackendAPI 72 | python manage.py collectstatic 73 | ``` 74 | 75 | ### Apply migrations 76 | ``` 77 | python manage.py migrate 78 | ``` 79 | 80 | ### Create super user 81 | ``` 82 | python manage.py createsuperuser 83 | ``` 84 | 85 | ### Run server 86 | ``` 87 | python manage.py runserver 88 | ``` 89 | 90 | ### Access 91 | 92 | > http://127.0.0.1:8000/admin/ 93 | 94 | `Note: If any item in the template is changed, it will be necessary to remove the static folder from .gitignore file` 95 | 96 | 97 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 2 | astroid==3.0.3 3 | attrs==23.2.0 4 | certifi==2024.2.2 5 | cffi==1.16.0 6 | charset-normalizer==3.3.2 7 | cryptography==42.0.2 8 | defusedxml==0.8.0rc2 9 | dill==0.3.8 10 | Django==5.0.2 11 | django-cors-headers==4.3.1 12 | django-debug-toolbar==4.3.0 13 | django-otp==1.3.0 14 | django-request-logging==0.7.5 15 | django-templated-mail==1.1.1 16 | djangorestframework==3.14.0 17 | djangorestframework-jwt-5==1.13.0 18 | djangorestframework-simplejwt==5.3.1 19 | djoser==2.2.2 20 | drf-spectacular-sidecar==2024.2.1 21 | drf-yasg==1.21.7 22 | idna==3.6 23 | inflection==0.5.1 24 | isort==5.13.2 25 | jsonschema==4.21.1 26 | jsonschema-specifications==2023.12.1 27 | lazy-object-proxy==1.10.0 28 | mccabe==0.7.0 29 | oauthlib==3.2.2 30 | packaging==23.2 31 | pillow==10.2.0 32 | platformdirs==4.2.0 33 | pycparser==2.21 34 | PyJWT==2.8.0 35 | pylint==3.0.3 36 | python-decouple==3.8 37 | python3-openid==3.2.0 38 | pytz==2024.1 39 | PyYAML==6.0.1 40 | referencing==0.33.0 41 | requests==2.31.0 42 | requests-oauthlib==1.3.1 43 | rpds-py==0.17.1 44 | six==1.16.0 45 | social-auth-app-django==5.4.0 46 | social-auth-core==4.5.2 47 | sqlparse==0.4.4 48 | tomlkit==0.12.3 49 | typed-ast==1.5.5 50 | uritemplate==4.1.1 51 | urllib3==2.2.0 52 | wrapt==1.16.0 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements-dev.txt 2 | gunicorn 3 | psycopg2 --------------------------------------------------------------------------------