././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/LICENSE 0000644 0000000 0000000 00000002073 15034742110 010737 0 ustar 00 The MIT License (MIT)
Copyright (c) 2013-2023 SUNSCRAPERS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/README.rst 0000644 0000000 0000000 00000010410 15034742110 011413 0 ustar 00 ======
djoser
======
.. image:: https://img.shields.io/pypi/v/djoser.svg
:target: https://pypi.org/project/djoser
.. image:: https://github.com/sunscrapers/djoser/actions/workflows/test-suite.yml/badge.svg?branch=master
:target: https://github.com/sunscrapers/djoser/actions?query=branch%3Amaster
:alt: Build Status
.. image:: https://codecov.io/gh/sunscrapers/djoser/branch/master/graph/badge.svg
:target: https://codecov.io/gh/sunscrapers/djoser
.. image:: https://img.shields.io/pypi/dm/djoser
:target: https://img.shields.io/pypi/dm/djoser
.. image:: https://readthedocs.org/projects/djoser/badge/?version=latest
:target: https://djoser.readthedocs.io/en/latest/
:alt: Docs
REST implementation of `Django `_ authentication
system. **djoser** library provides a set of `Django Rest Framework `_
views to handle basic actions such as registration, login, logout, password
reset and account activation. It works with
`custom user model `_.
Supported features include:
- Token-based authentication
- JWT authentication
- Social authentication
- WebAuthn support
Instead of reusing Django code (e.g. ``PasswordResetForm``), we reimplemented
few things to fit better into `Single Page App `_
architecture.
Developed by `SUNSCRAPERS `_ with passion & patience.
.. image:: https://asciinema.org/a/94J4eG2tSBD2iEfF30a6vGtXw.png
:target: https://asciinema.org/a/94J4eG2tSBD2iEfF30a6vGtXw
Requirements
============
To be able to run **djoser** you have to meet the following requirements:
- Python>=3.9,<4.0 (including 3.10, 3.11, and 3.12)
- Django>=3.0.0 (supporting Django 3.2 through 5.1)
- Django REST Framework>=3.12
Installation
============
Simply install using ``pip``:
.. code-block:: bash
$ pip install djoser
And continue with the steps described at
`configuration `_
guide.
Documentation
=============
Documentation is available to study at
`https://djoser.readthedocs.io `_
and in ``docs`` directory.
Contributing and development
============================
To start developing on **djoser**, clone the repository:
.. code-block:: bash
$ git clone git@github.com:sunscrapers/djoser.git
We use `poetry `_ as dependency management and packaging tool.
.. code-block:: bash
$ cd djoser
$ poetry install --all-extras
This will create a virtualenv with all development dependencies.
To run the test just type:
.. code-block:: bash
$ poetry run pytest
We also prepared a convenient ``Makefile`` to automate commands above:
.. code-block:: bash
$ make init
$ make test
To activate the virtual environment run
.. code-block:: bash
$ poetry shell
Without poetry
--------------
New versions of ``pip`` can use ``pyproject.toml`` to build the package and install its dependencies.
.. code-block:: bash
$ pip install .[test]
.. code-block:: bash
$ cd testproject
$ ./manage.py test
Example project
---------------
You can also play with test project by running following commands:
.. code-block:: bash
$ make migrate
$ make runserver
Commiting your code
-------------------
Before sending patches please make sure you have `pre-commit `_ activated in your local git repository:
.. code-block:: bash
$ poetry run pre-commit install
This will ensure that your code is cleaned before you commit it. The pre-commit hooks will run:
- Black (code formatting)
- Ruff (linting)
- Docformatter (docstring formatting)
- Other quality checks
Similar projects
================
List of projects related to Django, REST and authentication:
- `django-rest-registration `_
- `django-oauth-toolkit `_
Please, keep in mind that while using custom authentication and TokenCreateSerializer
validation, there is a path that **ignores intentional return of None** from authenticate()
and try to find User using parameters. Probably, that will be changed in the future.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/__init__.py 0000644 0000000 0000000 00000000026 15034742110 013325 0 ustar 00 __version__ = "2.2.0"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/auth_backends.py 0000644 0000000 0000000 00000002030 15034742110 014356 0 ustar 00 from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from djoser.conf import settings
UserModel = get_user_model()
class LoginFieldBackend(ModelBackend):
"""Allows to log in by a different value than the default Django
USERNAME_FIELD."""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
get_kwargs = {
settings.LOGIN_FIELD: username,
}
try:
user = UserModel._default_manager.get(**get_kwargs)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/compat.py 0000644 0000000 0000000 00000000413 15034742110 013051 0 ustar 00 from djoser.conf import settings
__all__ = ["settings"]
def get_user_email(user):
email_field_name = get_user_email_field_name(user)
return getattr(user, email_field_name, None)
def get_user_email_field_name(user):
return user.get_email_field_name()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/conf.py 0000644 0000000 0000000 00000015773 15034742110 012532 0 ustar 00 # flake8: noqa E501
from django.apps import apps
from django.conf import settings as django_settings
from django.test.signals import setting_changed
from django.utils.functional import LazyObject
from django.utils.module_loading import import_string
DJOSER_SETTINGS_NAMESPACE = "DJOSER"
auth_module, user_model = django_settings.AUTH_USER_MODEL.rsplit(".", 1)
User = apps.get_model(auth_module, user_model)
class ObjDict(dict):
def __getattribute__(self, item):
try:
val = self[item]
if isinstance(val, str):
val = import_string(val)
elif isinstance(val, (list, tuple)):
val = [import_string(v) if isinstance(v, str) else v for v in val]
self[item] = val
except KeyError:
val = super().__getattribute__(item)
return val
default_settings = {
"USER_ID_FIELD": User._meta.pk.name,
"LOGIN_FIELD": User.USERNAME_FIELD,
"SEND_ACTIVATION_EMAIL": False,
"SEND_CONFIRMATION_EMAIL": False,
"USER_CREATE_PASSWORD_RETYPE": False,
"SET_PASSWORD_RETYPE": False,
"PASSWORD_RESET_CONFIRM_RETYPE": False,
"SET_USERNAME_RETYPE": False,
"USERNAME_RESET_CONFIRM_RETYPE": False,
"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": False,
"USERNAME_RESET_SHOW_EMAIL_NOT_FOUND": False,
"PASSWORD_CHANGED_EMAIL_CONFIRMATION": False,
"USERNAME_CHANGED_EMAIL_CONFIRMATION": False,
"TOKEN_MODEL": "rest_framework.authtoken.models.Token",
"SERIALIZERS": ObjDict(
{
"activation": "djoser.serializers.ActivationSerializer",
"password_reset": "djoser.serializers.SendEmailResetSerializer",
"password_reset_confirm": "djoser.serializers.PasswordResetConfirmSerializer",
"password_reset_confirm_retype": "djoser.serializers.PasswordResetConfirmRetypeSerializer",
"set_password": "djoser.serializers.SetPasswordSerializer",
"set_password_retype": "djoser.serializers.SetPasswordRetypeSerializer",
"set_username": "djoser.serializers.SetUsernameSerializer",
"set_username_retype": "djoser.serializers.SetUsernameRetypeSerializer",
"username_reset": "djoser.serializers.SendEmailResetSerializer",
"username_reset_confirm": "djoser.serializers.UsernameResetConfirmSerializer",
"username_reset_confirm_retype": "djoser.serializers.UsernameResetConfirmRetypeSerializer",
"user_create": "djoser.serializers.UserCreateSerializer",
"user_create_password_retype": "djoser.serializers.UserCreatePasswordRetypeSerializer",
"user_delete": "djoser.serializers.UserDeleteSerializer",
"user": "djoser.serializers.UserSerializer",
"current_user": "djoser.serializers.UserSerializer",
"token": "djoser.serializers.TokenSerializer",
"token_create": "djoser.serializers.TokenCreateSerializer",
"provider_auth": "djoser.social.serializers.ProviderAuthSerializer",
}
),
"EMAIL": ObjDict(
{
"activation": "djoser.email.ActivationEmail",
"confirmation": "djoser.email.ConfirmationEmail",
"password_reset": "djoser.email.PasswordResetEmail",
"password_changed_confirmation": "djoser.email.PasswordChangedConfirmationEmail",
"username_changed_confirmation": "djoser.email.UsernameChangedConfirmationEmail",
"username_reset": "djoser.email.UsernameResetEmail",
}
),
"EMAIL_FRONTEND_DOMAIN": None,
"EMAIL_FRONTEND_PROTOCOL": None,
"EMAIL_FRONTEND_SITE_NAME": None,
"CONSTANTS": ObjDict({"messages": "djoser.constants.Messages"}),
"LOGOUT_ON_PASSWORD_CHANGE": False,
"CREATE_SESSION_ON_LOGIN": False,
"SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy",
"SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [],
"HIDE_USERS": True,
"PERMISSIONS": ObjDict(
{
"activation": ["rest_framework.permissions.AllowAny"],
"password_reset": ["rest_framework.permissions.AllowAny"],
"password_reset_confirm": ["rest_framework.permissions.AllowAny"],
"set_password": ["djoser.permissions.CurrentUserOrAdmin"],
"username_reset": ["rest_framework.permissions.AllowAny"],
"username_reset_confirm": ["rest_framework.permissions.AllowAny"],
"set_username": ["djoser.permissions.CurrentUserOrAdmin"],
"user_create": ["rest_framework.permissions.AllowAny"],
"user_delete": ["djoser.permissions.CurrentUserOrAdmin"],
"user": ["djoser.permissions.CurrentUserOrAdmin"],
"user_list": ["djoser.permissions.CurrentUserOrAdmin"],
"token_create": ["rest_framework.permissions.AllowAny"],
"token_destroy": ["rest_framework.permissions.IsAuthenticated"],
}
),
"WEBAUTHN": ObjDict(
{
"RP_NAME": "localhost",
"RP_ID": "localhost",
"ORIGIN": "http://localhost:8000",
"CHALLENGE_LENGTH": 32,
"UKEY_LENGTH": 20,
"SIGNUP_SERIALIZER": "djoser.webauthn.serializers.WebauthnCreateUserSerializer",
"LOGIN_SERIALIZER": "djoser.webauthn.serializers.WebauthnLoginSerializer",
}
),
}
SETTINGS_TO_IMPORT = ["TOKEN_MODEL", "SOCIAL_AUTH_TOKEN_STRATEGY"]
class Settings:
def __init__(self, default_settings, explicit_overriden_settings: dict = None):
if explicit_overriden_settings is None:
explicit_overriden_settings = {}
overriden_settings = (
getattr(django_settings, DJOSER_SETTINGS_NAMESPACE, {})
or explicit_overriden_settings
)
self._load_default_settings()
self._override_settings(overriden_settings)
self._init_settings_to_import()
def _load_default_settings(self):
for setting_name, setting_value in default_settings.items():
if setting_name.isupper():
setattr(self, setting_name, setting_value)
def _override_settings(self, overriden_settings: dict):
for setting_name, setting_value in overriden_settings.items():
value = setting_value
if isinstance(setting_value, dict):
value = getattr(self, setting_name, {})
value.update(ObjDict(setting_value))
setattr(self, setting_name, value)
def _init_settings_to_import(self):
for setting_name in SETTINGS_TO_IMPORT:
value = getattr(self, setting_name)
if isinstance(value, str):
setattr(self, setting_name, import_string(value))
class LazySettings(LazyObject):
def _setup(self, explicit_overriden_settings=None):
self._wrapped = Settings(default_settings, explicit_overriden_settings)
settings = LazySettings()
def reload_djoser_settings(*args, **kwargs):
global settings
setting, value = kwargs["setting"], kwargs["value"]
if setting == DJOSER_SETTINGS_NAMESPACE:
settings._setup(explicit_overriden_settings=value)
setting_changed.connect(reload_djoser_settings)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/constants.py 0000644 0000000 0000000 00000001376 15034742110 013613 0 ustar 00 from django.utils.translation import gettext_lazy as _
class Messages:
INVALID_CREDENTIALS_ERROR = _("Unable to log in with provided credentials.")
INACTIVE_ACCOUNT_ERROR = _(
"User account is disabled."
) # not in use since Django 1.10
INVALID_TOKEN_ERROR = _("Invalid token for given user.")
INVALID_UID_ERROR = _("Invalid user id or user doesn't exist.")
STALE_TOKEN_ERROR = _("Stale token for given user.")
PASSWORD_MISMATCH_ERROR = _("The two password fields didn't match.")
USERNAME_MISMATCH_ERROR = _("The two {0} fields didn't match.")
INVALID_PASSWORD_ERROR = _("Invalid password.")
EMAIL_NOT_FOUND = _("User with given email does not exist.")
CANNOT_CREATE_USER_ERROR = _("Unable to create account.")
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417351.5879755
djoser-2.3.3/djoser/email.py 0000644 0000000 0000000 00000012764 15034742110 012671 0 ustar 00 from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from djoser import utils
from django.conf import settings as django_settings
from djoser.conf import settings
from django.core import mail
from django.template.context import make_context
from django.template.loader import get_template
from django.views.generic.base import ContextMixin
class BaseEmailMessage(mail.EmailMultiAlternatives, ContextMixin):
_node_map = {
"subject": "subject",
"text_body": "body",
"html_body": "html",
}
template_name = None
def __init__(self, request=None, context=None, template_name=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.context = {} if context is None else context
self.html = None
if template_name is not None:
self.template_name = template_name
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
context = dict(ctx, **self.context)
if self.request:
site = get_current_site(self.request)
domain = context.get("domain") or (
getattr(django_settings, "DOMAIN", "") or site.domain
)
protocol = context.get("protocol") or (
"https" if self.request.is_secure() else "http"
)
site_name = context.get("site_name") or (
getattr(django_settings, "SITE_NAME", "") or site.name
)
user = context.get("user") or self.request.user
else:
domain = context.get("domain") or getattr(django_settings, "DOMAIN", "")
protocol = context.get("protocol") or "http"
site_name = context.get("site_name") or getattr(
django_settings, "SITE_NAME", ""
)
user = context.get("user")
context.update(
{
"domain": domain,
"protocol": protocol,
"site_name": site_name,
"user": user,
}
)
return context
def render(self):
context = make_context(self.get_context_data(), request=self.request)
template = get_template(self.template_name)
with context.bind_template(template.template):
for node in template.template.nodelist:
self._process_node(node, context)
self._attach_body()
# custom interface incompatible with django, `to` is a required param
def send(self, to, fail_silently=False, **kwargs):
self.render()
self.to = to
self.cc = kwargs.pop("cc", [])
self.bcc = kwargs.pop("bcc", [])
self.reply_to = kwargs.pop("reply_to", [])
self.from_email = kwargs.pop("from_email", django_settings.DEFAULT_FROM_EMAIL)
self.request = None
super().send(fail_silently=fail_silently)
def _process_node(self, node, context):
attr = self._node_map.get(getattr(node, "name", ""))
if attr is not None:
setattr(self, attr, node.render(context).strip())
def _attach_body(self):
if self.body and self.html:
self.attach_alternative(self.html, "text/html")
elif self.html:
self.body = self.html
self.content_subtype = "html"
class BaseDjoserEmail(BaseEmailMessage):
def get_context_data(self):
context = super().get_context_data()
overridable = {
"protocol": settings.EMAIL_FRONTEND_PROTOCOL,
"domain": settings.EMAIL_FRONTEND_DOMAIN,
"site_name": settings.EMAIL_FRONTEND_SITE_NAME,
}
for context_key, context_value in overridable.items():
if context_value:
context.update({context_key: context_value})
context.pop("view", None)
return context
class ActivationEmail(BaseDjoserEmail):
template_name = "email/activation.html"
def get_context_data(self):
# ActivationEmail can be deleted
context = super().get_context_data()
user = context.get("user")
context["uid"] = utils.encode_uid(user.pk)
context["token"] = default_token_generator.make_token(user)
context["url"] = settings.ACTIVATION_URL.format(**context)
return context
class ConfirmationEmail(BaseDjoserEmail):
template_name = "email/confirmation.html"
class PasswordResetEmail(BaseDjoserEmail):
template_name = "email/password_reset.html"
def get_context_data(self):
# PasswordResetEmail can be deleted
context = super().get_context_data()
user = context.get("user")
context["uid"] = utils.encode_uid(user.pk)
context["token"] = default_token_generator.make_token(user)
context["url"] = settings.PASSWORD_RESET_CONFIRM_URL.format(**context)
return context
class PasswordChangedConfirmationEmail(BaseDjoserEmail):
template_name = "email/password_changed_confirmation.html"
class UsernameChangedConfirmationEmail(BaseDjoserEmail):
template_name = "email/username_changed_confirmation.html"
class UsernameResetEmail(BaseDjoserEmail):
template_name = "email/username_reset.html"
def get_context_data(self):
context = super().get_context_data()
user = context.get("user")
context["uid"] = utils.encode_uid(user.pk)
context["token"] = default_token_generator.make_token(user)
context["url"] = settings.USERNAME_RESET_CONFIRM_URL.format(**context)
return context
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1752417360.7109857
djoser-2.3.3/djoser/locale/ca/LC_MESSAGES/django.mo 0000644 0000000 0000000 00000007140 15034742121 016430 0 ustar 00 I <