From f0f4a55e16113f1601aa149eab4c896773ab0812 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Sun, 30 Jul 2023 19:51:41 +0100 Subject: [PATCH] feat: implements tenant configuration --- apps/portfolio/admin.py | 8 +- apps/portfolio/migrations/0001_initial.py | 2 +- apps/users/migrations/0001_initial.py | 2 +- apps/users/models/base_model.py | 36 ++++---- apps/users/models/tenant_model.py | 1 + apps/users/models/users.py | 87 +++++++++++++------ apps/users/serializers/auth_serializers.py | 2 +- apps/users/views/__init__.py | 1 + apps/users/views/auth_views.py | 3 + apps/users/views/default.py | 14 ++++ documentation/auth/login_docs.html | 0 visuleo_port/settings/base.py | 3 +- visuleo_port/settings/extra.py | 27 +++++- visuleo_port/urls.py | 1 + visuleo_port/urls_public.py | 97 ++++++++++++++++++++++ 15 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 apps/users/views/default.py create mode 100644 documentation/auth/login_docs.html create mode 100644 visuleo_port/urls_public.py diff --git a/apps/portfolio/admin.py b/apps/portfolio/admin.py index 8c38f3f..cc39dc0 100644 --- a/apps/portfolio/admin.py +++ b/apps/portfolio/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin -# Register your models here. +from apps.users.models.users import VisuleoUser + +admin.site.site_header = "Visuleo Admin" +admin.site.site_title = "Visuleo Admin Portal" +admin.site.index_title = "Welcome to Visuleo Portal" + +admin.site.register(VisuleoUser) diff --git a/apps/portfolio/migrations/0001_initial.py b/apps/portfolio/migrations/0001_initial.py index 927784a..d320e56 100644 --- a/apps/portfolio/migrations/0001_initial.py +++ b/apps/portfolio/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-07-29 12:07 +# Generated by Django 4.1.7 on 2023-07-30 16:31 from django.db import migrations, models import utils.main diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py index 23321be..dc59a91 100644 --- a/apps/users/migrations/0001_initial.py +++ b/apps/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-07-29 12:07 +# Generated by Django 4.1.7 on 2023-07-30 16:31 import django.contrib.auth.validators from django.db import migrations, models diff --git a/apps/users/models/base_model.py b/apps/users/models/base_model.py index 6135335..c95a1b5 100644 --- a/apps/users/models/base_model.py +++ b/apps/users/models/base_model.py @@ -8,21 +8,22 @@ class BaseModelDeletionManager(models.Manager): Custom model manager that returns all objects, including those that are deleted. """ + def get_queryset(self): return super().get_queryset() - + def all_with_deleted(self): return super().get_queryset().filter(is_deleted=True) - -class BaseMdeoelManager(models.Manager): +class BaseModelManager(models.Manager): """ Custom model manager that returns only objects that are not deleted. """ + def get_queryset(self): return super().get_queryset().filter(is_deleted=False) - + def all_with_deleted(self): return super().get_queryset() @@ -32,34 +33,37 @@ class BaseModel(models.Model): Abstract model that provides self-updating ``created`` and ``modified`` fields. """ + id = models.UUIDField( - _('id'), + _("id"), primary_key=True, default=generate_uuid, editable=False, - help_text=_('Unique identifier for this object.'), + help_text=_("Unique identifier for this object."), ) created = models.DateTimeField( - _('created'), + _("created"), auto_now_add=True, - help_text=_('Date and time when this object was created.'), + help_text=_("Date and time when this object was created."), ) modified = models.DateTimeField( - _('modified'), + _("modified"), auto_now=True, - help_text=_('Date and time when this object was last modified.'), + help_text=_("Date and time when this object was last modified."), ) is_deleted = models.BooleanField( - _('is deleted'), + _("is deleted"), default=False, - help_text=_('Boolean field to mark if this object is deleted.'), + help_text=_("Boolean field to mark if this object is deleted."), ) - - objects = BaseMdeoelManager() - deleted_obkects = BaseModelDeletionManager() + objects = BaseModelManager() + deleted_obkects = BaseModelDeletionManager() class Meta: abstract = True - ordering = ('-created', '-modified',) \ No newline at end of file + ordering = ( + "-created", + "-modified", + ) diff --git a/apps/users/models/tenant_model.py b/apps/users/models/tenant_model.py index 36b3cec..8ae772f 100644 --- a/apps/users/models/tenant_model.py +++ b/apps/users/models/tenant_model.py @@ -6,6 +6,7 @@ class Client(TenantMixin): paid_until = models.DateField() on_trial = models.BooleanField() created_on = models.DateField(auto_now_add=True) + auto_create_schema = True def __str__(self)->str: return self.name diff --git a/apps/users/models/users.py b/apps/users/models/users.py index 660623c..87da83f 100644 --- a/apps/users/models/users.py +++ b/apps/users/models/users.py @@ -1,70 +1,103 @@ from django.db import models -from django.contrib.auth.models import AbstractUser +from django.contrib.auth.models import AbstractUser, BaseUserManager from django.utils.translation import gettext_lazy as _ from django.utils import timezone from apps.users.models import BaseModel +class VisuleoUserManager(BaseUserManager): + """ + Custom user model manager for Visuleo. + """ + + def create_user(self, email, password, **extra_fields): + """ + Create and save a Visuleo user with the given email and password. + """ + if not email: + raise ValueError(_("The email must be set.")) + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_superuser(self, email, password, **extra_fields): + """ + Create and save a Visuleo superuser with the given email and password. + """ + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + extra_fields.setdefault("is_email_verified", True) + extra_fields.setdefault("is_phone_number_verified", True) + return self.create_user(email, password, **extra_fields) + + class VisuleoUser(BaseModel, AbstractUser): """ Custom user model for Visuleo. """ + name = models.CharField( - _('name'), + _("name"), max_length=255, blank=True, - help_text=_('User\'s full name.'), + help_text=_("User's full name."), ) email = models.EmailField( - _('email address'), + _("email address"), unique=True, - help_text=_('User\'s email address.'), + help_text=_("User's email address."), ) phone_number = models.CharField( - _('phone number'), + _("phone number"), max_length=20, blank=True, - help_text=_('User\'s phone number.'), + help_text=_("User's phone number."), ) is_email_verified = models.BooleanField( - _('is email verified'), + _("is email verified"), default=False, - help_text=_('Boolean field to mark if this user\'s email is verified.'), + help_text=_("Boolean field to mark if this user's email is verified."), ) is_phone_number_verified = models.BooleanField( - _('is phone number verified'), + _("is phone number verified"), default=False, - help_text=_('Boolean field to mark if this user\'s phone number is verified.'), + help_text=_("Boolean field to mark if this user's phone number is verified."), ) is_active = models.BooleanField( - _('is active'), + _("is active"), default=True, - help_text=_('Boolean field to mark if this user is active.'), + help_text=_("Boolean field to mark if this user is active."), ) is_superuser = models.BooleanField( - _('is superuser'), + _("is superuser"), default=False, - help_text=_('Boolean field to mark if this user is superuser.'), + help_text=_("Boolean field to mark if this user is superuser."), ) last_login = models.DateTimeField( - _('last login'), + _("last login"), default=timezone.now, - help_text=_('Date and time when this user last logged in.'), + help_text=_("Date and time when this user last logged in."), ) date_joined = models.DateTimeField( - _('date joined'), + _("date joined"), default=timezone.now, - help_text=_('Date and time when this user joined.'), + help_text=_("Date and time when this user joined."), ) - - USERNAME_FIELD = 'email' + + objects = VisuleoUserManager() + + USERNAME_FIELD = "email" REQUIRED_FIELDS = [] - + class Meta: - verbose_name = _('user') - verbose_name_plural = _('users') - ordering = ('-created', '-modified',) - + verbose_name = _("user") + verbose_name_plural = _("users") + ordering = ( + "-created", + "-modified", + ) + def __str__(self) -> str: return self.email - diff --git a/apps/users/serializers/auth_serializers.py b/apps/users/serializers/auth_serializers.py index 2c1dbfb..3dd2c9f 100644 --- a/apps/users/serializers/auth_serializers.py +++ b/apps/users/serializers/auth_serializers.py @@ -54,7 +54,7 @@ class Meta: ) -class UserResponseSerializer(serializers.ModelSerializer): +class UserResponseSerializer(serializers.Serializer): """ Serializer for user. """ diff --git a/apps/users/views/__init__.py b/apps/users/views/__init__.py index 3cb1e42..24acb9a 100644 --- a/apps/users/views/__init__.py +++ b/apps/users/views/__init__.py @@ -1 +1,2 @@ from .auth_views import RegistrationView, LoginView, LogoutView +from .default import DefaultView diff --git a/apps/users/views/auth_views.py b/apps/users/views/auth_views.py index b147c26..e73d17c 100644 --- a/apps/users/views/auth_views.py +++ b/apps/users/views/auth_views.py @@ -21,6 +21,8 @@ from drf_yasg.utils import swagger_auto_schema from django.utils.timezone import now +from utils.main import load_document + User = get_user_model() @@ -118,6 +120,7 @@ def get_serializer(self, *args, **kwargs): @swagger_auto_schema( operation_id="Login", operation_summary="Login a user", + operation_description=load_document("auth/login_docs.html"), request_body=LoginSerializer, tags=["Authentication and Management"], responses={ diff --git a/apps/users/views/default.py b/apps/users/views/default.py new file mode 100644 index 0000000..a3c0efe --- /dev/null +++ b/apps/users/views/default.py @@ -0,0 +1,14 @@ +from rest_framework.views import APIView +from rest_framework.response import Response + + +class DefaultView(APIView): + """ + Default view for the users app. + """ + authentication_classes = () + permission_classes = () + + + def get(self, request): + return Response({"message": "No tenants found."}) diff --git a/documentation/auth/login_docs.html b/documentation/auth/login_docs.html new file mode 100644 index 0000000..e69de29 diff --git a/visuleo_port/settings/base.py b/visuleo_port/settings/base.py index 860bad7..ecc41e8 100644 --- a/visuleo_port/settings/base.py +++ b/visuleo_port/settings/base.py @@ -41,8 +41,7 @@ MIDDLEWARE = [ - "django_tenants.middleware.main.TenantMainMiddleware", - "django_tenants.middleware.TenantSubfolderMiddleware", + 'django_tenants.middleware.main.TenantMainMiddleware', "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", diff --git a/visuleo_port/settings/extra.py b/visuleo_port/settings/extra.py index aeeb311..244c0e8 100644 --- a/visuleo_port/settings/extra.py +++ b/visuleo_port/settings/extra.py @@ -3,12 +3,33 @@ # djanfo tenant conf TENANT_MODEL = "users.Client" TENANT_DOMAIN_MODEL = "users.Domain" -TENANT_SUBFOLDER_PREFIX = "clients" DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",) -TENANT_APPS = ["apps.portfolio.apps.PortfolioConfig", "apps.users.apps.UsersConfig"] -DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"] +TENANT_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "apps.portfolio.apps.PortfolioConfig", + "apps.users.apps.UsersConfig", + "apps.portfolio.apps.PortfolioConfig", + "apps.users.apps.UsersConfig", + "rest_framework", + "oauth2_provider", + "drf_yasg", + "django_filters", + "corsheaders", + "simple_history", + "allauth", + "allauth.account", + "storages", +] +DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",) + +PUBLIC_SCHEMA_URLCONF = "visuleo_port.urls_public" SHARED_APPS = [ "django.contrib.admin", diff --git a/visuleo_port/urls.py b/visuleo_port/urls.py index 74ee842..91a928c 100644 --- a/visuleo_port/urls.py +++ b/visuleo_port/urls.py @@ -92,4 +92,5 @@ path("admin/", admin.site.urls), path(ROUTE_BASE_VERSION, include("apps.portfolio.routes.api")), path(ROUTE_BASE_VERSION, include("apps.users.routes.api")), + path('api-auth/', include('rest_framework.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/visuleo_port/urls_public.py b/visuleo_port/urls_public.py new file mode 100644 index 0000000..36591c1 --- /dev/null +++ b/visuleo_port/urls_public.py @@ -0,0 +1,97 @@ +import oauth2_provider.views as oauth2_views +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, re_path, include +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions +from apps.users.views import DefaultView + +from utils.main import load_document + + +schema_view = get_schema_view( + openapi.Info( + title="Visuleo API", + default_version="v1", + description=load_document("base/index.md"), + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="yokwejuste@gmail.com"), + license=openapi.License(name="GNUX License"), + ), + public=True, + permission_classes=[permissions.AllowAny], +) + +ROUTE_BASE_VERSION = "api/v0/" + +oauth2_endpoint_views = [ + path("authorize/", oauth2_views.AuthorizationView.as_view(), name="authorize"), + path("token/", oauth2_views.TokenView.as_view(), name="token"), + path("revoke-token/", oauth2_views.RevokeTokenView.as_view(), name="revoke-token"), +] + +if settings.DEBUG: + oauth2_endpoint_views += [ + path("applications/", oauth2_views.ApplicationList.as_view(), name="list"), + path( + "applications/register/", + oauth2_views.ApplicationRegistration.as_view(), + name="register", + ), + path( + "applications//", + oauth2_views.ApplicationDetail.as_view(), + name="detail", + ), + path( + "applications//delete/", + oauth2_views.ApplicationDelete.as_view(), + name="delete", + ), + path( + "applications//update/", + oauth2_views.ApplicationUpdate.as_view(), + name="update", + ), + ] + + # OAuth2 Token Management endpoints + oauth2_endpoint_views += [ + path( + "authorized-tokens/", + oauth2_views.AuthorizedTokensListView.as_view(), + name="authorized-token-list", + ), + path( + "authorized-tokens//delete/", + oauth2_views.AuthorizedTokenDeleteView.as_view(), + name="authorized-token-delete", + ), + ] + + +urlpatterns = [ + path( + "o/", + include( + (oauth2_endpoint_views, "oauth2_provider"), namespace="oauth2_provider" + ), + ), + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + re_path( + r"^swagger/$", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + re_path(r"$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + path("admin/", admin.site.urls), + path(ROUTE_BASE_VERSION, include("apps.portfolio.routes.api")), + path(ROUTE_BASE_VERSION, include("apps.users.routes.api")), + re_path(r"$", DefaultView.as_view(), name="default"), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)