Metadata-Version: 2.4
Name: ngits-users
Version: 2.1.0
Summary: Base users application for Django projects
Author: NGITs
License: BSD-3-Clause
Project-URL: Homepage, https://ngits.dev
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Requires-Python: >=3.11
Description-Content-Type: text/x-rst
Requires-Dist: Django==5.*
Requires-Dist: djangorestframework==3.*
Requires-Dist: drf-spectacular==0.27.*
Requires-Dist: requests==2.*
Requires-Dist: celery==5.*
Requires-Dist: redis==5.*
Requires-Dist: django-phonenumber-field[phonenumberslite]==8.*
Requires-Dist: Pillow==10.*
Requires-Dist: pillow-heif==0.18.*

ngits-users
============

Base ‘users’ application for Django projects. It provides following endpoints:

    - Registration
    - Background registration
    - Login
    - Change password
    - Change email
    - Remind password
    - Delete account
    - Google authentication
    - Facebook authentication

... and following template views:

    - Verify account
    - Confirm password remind


Setup
-----

1. Install using pip:
~~~~~~~~~~~~~~~~~~~~~

::

       pip install ngits-users

2. Change your ``settings`` file:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

::

       import os

       ...

       INSTALLED_APPS = [
           ...
           "rest_framework",
           "rest_framework.authtoken",
           "users"
       ]

       ...

       AUTH_USER_MODEL = "users.User"

       CELERY_BROKER_URL = "<redis_url>"
       CELERY_RESULT_BACKEND = "<redis_url>"

       DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "<your_email>")

       REST_FRAMEWORK = {
           "DEFAULT_AUTHENTICATION_CLASSES": [
               "rest_framework.authentication.TokenAuthentication",
           ],
           # Optional
           "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
       }

       REGISTRATION_EMAIL_SUBJECT = "<email subject>"
       REMIND_EMAIL_SUBJECT = "<email subject>"

       # debugging
       EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

3. Add paths to your ``urls.py`` file:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

::

       from django.urls import path, include

       urlpatterns = [
           ...
           path("users/", include("users.urls"))
       ]

4. Run migrations:
~~~~~~~~~~~~~~~~~~

::

       py manage.py migrate

5. Add following variables to your ``.env`` file:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

::

      # smpt config
      DEFAULT_FROM_EMAIL=no-reply@ngits.dev

      EMAIL_HOST=
      EMAIL_HOST_PASSWORD=
      EMAIL_HOST_USER=
      EMAIL_PORT=

      # celery
      CELERY_BROKER_URL=
      CELERY_RESULT_BACKEND=

6. Celery configuration:
~~~~~~~~~~~~~~~~~~~~~~~~

``../<django_project>/<proj_name>/celery.py``

::


       import os

       from celery import Celery

       os.environ.setdefault("DJANGO_SETTINGS_MODULE", "<proj_name>.settings")

       app = Celery("<proj_name>")
       app.config_from_object("django.conf:settings", namespace="CELERY")
       app.autodiscover_tasks()

``../<django_project>/<proj_name>/__init__.py``

::

      from .celery import app as celery_app

      __all__ = ("celery_app",)

7. Optional ``redoc`` configuration:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

::

       pip install drf-spectacular==0.23.*

``settings.py``:

::

       INSTALLED_APPS = [
           ...
           "drf_spectacular"
       ]

       SPECTACULAR_SETTINGS = {
           "TITLE": "<proj_name> API",
           "VERSION": "1.0.0",
       }

       TEMPLATES = [
           ...
           'DIRS': [ BASE_DIR / "templates"],
           ...
       ]

``urls.py``:

::

        from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView

        ...

        urlpatterns = [
            ...
            path(
                "docs/schema/",
                SpectacularAPIView.as_view(),
                name="schema"
            ),
            path(
                "docs/redoc/",
                SpectacularRedocView.as_view(url_name="schema"),
                name="redoc",
            ),
        ]

``../<django_project>/templates/redoc.html``:

::

       <!DOCTYPE html>
       <html>
           <head>
               <title>ReDoc</title>
               <!-- needed for adaptive design -->
               <meta charset="utf-8"/>
               <meta name="viewport" content="width=device-width, initial-scale=1">
               <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
               <!-- ReDoc doesn't change outer page styles -->
               <style>
                   body {
                       margin: 0;
                       padding: 0;
                   }
               </style>
           </head>
           <body>
               <redoc spec-url='{% url schema_url %}'></redoc>
               <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
           </body>
       </html>

Finally generate YAML schema of documentation:

::

    py manage.py spectacular --file schema.yml


8. Optional ``templates`` override:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to override the default templates you have to create new files in your configured templates directory named:
    - Email templates: **these should contain {{ url|safe }}**
        - ``change_password_email.html``
        - ``change_password_email.txt``
        - ``registration_email.html``
        - ``registration_email.txt``
    - View templates:
        - ``change_password.html`` - **this have to contain {{ form }} !**
        - ``verify_ok.html``
        - ``verify_error.html``

There's also additional :code:`{{ email }}` context param you can use in your email templates.

e.g.:

::

    /repo
        /manage.py
        /templates
            /change_password_email.html
            /change_password_email.txt
            /change_password.html

*For fore details check out library default templates*

9. Optional ``TokenSerializer`` override:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can override ``TokenSerializer`` - the default response serializer on ``LoginView`` `(/login)`.

In order to use your own serializer, you need to follow these steps:

1. Create your custom serializer:

e.g.:

::

    from rest_framework import serializers
    from rest_framework.authtoken.models import Token

    ...

    class TestSerializer(serializers.ModelSerializer):
        foo = serializers.SerializerMethodField()

        class Meta:
            model = Token
            fields = ("key", "user_id", "foo")

        def get_foo(self, obj):
            return "bar"

**Warning!** Your custom serializer must handle incoming DRF ``Token`` object!

2. Set serializer path in your ``settings`` file

e.g.:

::

    LOGIN_RESPONSE_SERIALIZER_PATH = "app.serializers.TestSerializer"

3. Take it for a spin!

::

    HTTP 200 OK
    Allow: POST, OPTIONS
    Content-Type: application/json
    Vary: Accept

    {
        "key": "a5851e7359d1d04cd99a26014e47fcbedaa0beea",
        "user_id": 1,
        "foo": "bar"
    }

10. Optional ``AvatarDownloadView`` access checker:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can override access verification for ``AvatarDownloadView`` `(/<user_id>/avatar/)`.

By default, every authenticated user can download another user's avatar. If you need custom rules
(e.g. tenant isolation, organization membership, ownership), define your own checker and point to it
from settings.

In the checker:

- ``request.user`` is the authenticated user making the request
- ``target_user`` is the user selected by ``<user_id>`` in the URL, whose avatar is being downloaded

1. Create your custom checker:

e.g.:

::

    def check_same_tenant(request, target_user):
        return request.user.tenant_id == target_user.tenant_id

The checker must accept ``(request, target_user)`` and return ``True`` when access should be allowed.

2. Set checker path in your ``settings`` file

e.g.:

::

    AVATAR_ACCESS_CHECKER_PATH = "app.permissions.check_same_tenant"

3. Take it for a spin!

::

    GET /users/12/avatar/
    Authorization: Token <your_token>

If the checker returns ``False``, the endpoint responds with:

::

    HTTP 403 Forbidden
    Allow: GET, HEAD, OPTIONS
    Content-Type: application/json
    Vary: Accept

    {
        "detail": "Access denied."
    }

Login response codes
--------------------

400 response:

+---------------+--------------------+
| error_code    | error_msg          |
+===============+====================+
| 00            | Login failed       |
+---------------+--------------------+
| 01            | User not found     |
+---------------+--------------------+
| 02            | User not active    |
+---------------+--------------------+

Additional information
----------------------

This package also support *django tranlations*.
