Name | wc-django-2factor JSON |
Version |
0.2.1
JSON |
| download |
home_page | None |
Summary | Package to create general API for 2factor checkers. |
upload_time | 2024-12-05 13:43:55 |
maintainer | None |
docs_url | None |
author | WebCase |
requires_python | >=3.6 |
license | MIT License |
keywords |
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# WebCase 2factor API
Package to create general API for 2factor checkers.
## Installation
```sh
pip install wc-django-2factor
```
In `settings.py`:
```python
INSTALLED_APPS += [
# Dependencies:
'pxd_admin_extensions',
'django_jsonform',
'wcd_settings',
# 2Factor itself.
'wcd_2factor',
]
WCD_2FACTOR = {
# It will be empty by default:
'METHODS': [
# Simple builtin 2factor method.
# Used to work like user secret confirmation.
# But mostly serves as an example.
'wcd_2factor.builtins.dummy.DUMMY_METHOD_DESCRIPTOR',
],
# Custom json encoder
'JSON_ENCODER': 'wcd_2factor.utils.types.EnvoyerJSONEncoder',
}
```
## Usage
### Confirmer
Service for confirmation state management.
```python
from wcd_2factor.confirmer import default_confirmer, Backend, Confirmer
from wcd_2factor.registries import method_config_registry
from wcd_2factor.models import MethodConfig, UserConfig, ConfirmationState
# Default registry
# Use:
default confirmer
# Or create another.
confirmer = Confirmer(method_config_registry)
# List of all available method keys:
default_confirmer.get_methods()
# List of all active `MethodConfig` instances:
default_confirmer.get_method_configs()
# List of all user `UserConfig` configurations:
default_confirmer.get_user_configs(
# Provide user instance:
user=user or None,
# Or identifier:
user_id=user.pk or None
)
# Creates backend for some method config or `None` if there is no such:
default_confirmer.make_backend(
# Optional, if `user_config` will be provided, since it also has a
# relation to a MethodConfig.
method_config=MethodConfig() or None,
# Optional, since confirmation could be done just using the MethodConfig
# by itself.
user_config=UserConfig() or None,
# Optional context to be passed to the backend.
context={} or None,
# Whether should raise an exception if backend could not be created.
# For example when there is no registered method.
should_raise=False,
)
# Method to change user confirmation.
# Id will check if the changes are significant enough to request a
# confirmation from user.
# If it does - `make_confirmation` - will be a callable to create new
# `ConfirmationState` instance. Else `None` will be returned.
instance = UserConfig()
make_confirmation = default_confirmer.change_user_config(
# Current instance to apply changes to.
instance,
# New configuration object. Either a pydantic object or dataclass or just a
# simple dict, that will be internally converted to a pydantic object.
DTO() or dict(),
# If you already have an initialized backend, method could use it
# instead of creating a new one:
backend=Backend() or None,
# Optional method config object. If, for example `user_config` instance
# doesen't have one attached yet,
method_config=MethodConfig() or None,
# Optional context to be passed to the backend's method.
context={} or None,
)
# Don't forget to save your configuration instance.
# It will not be saved by this method.
instance.save()
if make_confirmation is not None:
confirmation: ConfirmationState = make_confirmation()
# Requesting any type of confirmation:
confirmation: ConfirmationState = default_confirmer.request_confirmation(
# Optional, if `user_config` will be provided, since it also has a
# relation to a MethodConfig.
method_config=MethodConfig() or None,
# Optional, since confirmation could be done just using the MethodConfig
# by itself.
user_config=UserConfig() or None,
# If you already have an initialized backend, method could use it
# instead of creating a new one:
backend=Backend() or None,
# User provided state.
# It depends on backend what kind of parameters should and should not be
# present.
# In most cases if `used_config` provided - no additional information
# required at all.
state={} or None,
# Optional context to be passed to the backend's method.
context={} or None,
)
# If you have user data to confirm some `ConfirmationState` run this:
confirmation: ConfirmationState = default_confirmer.confirm(
# Either identifier:
id=uuid4() or None,
# Or confirmation object itself must be provided:
confirmation=confirmation or None,
# User passed data, that confirms that user have control over the
# "second factor":
data={} or None,
# If you already have an initialized backend, method could use it
# instead of creating a new one:
backend=Backend() or None,
# Optional context to be passed to the backend's method.
context={} or None,
)
# Method might return state, event when confirmation process failed for some
# reason.
# So check the confirmation before using it:
if not confirmation.is_available():
raise ValueError('Confirmation failed.')
# Checks if confirmation is confirmed and available to use:
available, optional_confirmation = default_confirmer.check(
# Either identifier:
id=uuid4() or None,
# Or confirmation object itself must be provided:
confirmation=confirmation or None,
# Optional context to be passed to the backend's method.
context={} or None,
)
# In some cases method might return None instead of confirmation object.
# That happens when confirmation was already used, or there were none at all.
if not available:
raise ValueError('Confirmation unavailable.')
# And the last one.
# ConfirmationState object is a "one-time" password to perform some action.
# So after usage it will be deleted from the database.
used, optional_confirmation = default_confirmer.use(
# Either identifier:
id=uuid4() or None,
# Or confirmation object itself must be provided:
confirmation=confirmation or None,
# Optional context to be passed to the backend's method.
context={} or None,
)
# But you will still have object returned if `used` was true.
# You might need to do something with it afterwards.
if not used:
raise ValueError('Confirmation failed.')
```
### Registry and custom Backends
Registry is a simple dict with some additional methods to register new confirmation methods.
For every method that could be used in your application `MethodConfigDescriptor` should be defined and added to registry.
For example:
```python
from wcd_2factor.registries import (
method_config_registry, Registry,
MethodConfigDescriptor, DTO,
)
# This is a default method's registry.
# It will be autopopulated with descriptors from
# django_settings.WCD_2FACTOR['METHODS'].
method_config_registry
# But nothing stops you from creating your own registry.
my_registry = Registry()
# And after that you may add descriptors to it.
MY_METHOD_DESCRIPTOR = my_registry.register(MethodConfigDescriptor(
# Unique method key.
key='my_method',
# Verbose method name.
verbose_name=gettext_lazy('My Method'),
# Backend class is required, since it will be used to execute every
# `Confirmer` method.
backend_class=Backend,
# Other data object classes and schemas are optional:
# MethodConfig pydantic class.
# Configuration model for MethodConfig.
config_global_dto=BaseModel or None,
# JSONSchema for that configuration.
config_global_schema=BaseModel.model_json_schema() or None,
# Configuration model for UserConfig.
config_user_dto=BaseModel or None,
# JSONSchema for that configuration.
config_user_schema=BaseModel.model_json_schema() or None,
))
```
But descriptor is only a simple definition with and additional configuration inside.
All the work with message sending and request confirmation are on your `Backend` implementation.
```python
from wcd_2factor.confirmer import Backend
from wcd_2factor.registries import DTO, MethodConfigDescriptor
from wcd_2factor.models import ConfirmationState, UserConfig
class YourBackend(Backend):
method_config: YourMethodDTO
user_config: Optional[YourUserDTO]
# Method that checks if user configuration changed.
# And if this change is significant enough to request a confirmation.
def change_user_config(
self,
# New configuration to check for changes.
new: YourMethodDTO,
context: Optional[dict] = None,
) -> Tuple[bool, Optional[dict]]:
# Pseudocode:
if (
self.user_config is None
or
self.user_config != new
):
# Then user configuration changed and confirmation with
# some "state" should be created to confirm the change.
return True, {'some': 'state'}
# Otherwise - do nothing.
return False, None
# This is method for all confirmation requests creation.
# Whether it's for user confirmation or not, with empty `self.user_config`
# and only `self.method_config` available or "fully configured"".
def request_confirmation(
self,
# User or application provided state.
state: dict,
context: Optional[dict] = None,
) -> Tuple[ConfirmationState.Status, dict]:
return ConfirmationState.Status.IN_PROCESS, {
**state,
'some_confirmation_token_to_check': 'value',
}
# To confirm saved confirmation state, `Confirmer` will call this method.
def confirm(
self,
# State from `ConfirmationState` object.
state: dict,
# User-provided data to validate against.
user_data: Any,
context: Optional[dict] = None,
) -> bool:
# Return True if user provided something that is somehow valid
# against the stored state.
# User will never have access to the ConfigurationState data from
# your `request_confirmation` object.
# At least he should not.
return (
state.get('some_confirmation_token_to_check')
==
user_data.get('validation_token')
)
```
### Frontend/DRF
Library has an API implmentation based on DjangoRestFramework.
It is available in `wcd_2factor.contrib.dtf` module.
In `urls.py`:
```python
from wcd_2factor.contrib.drf.views import make_urlpatterns as twofactor_make_urlpatterns
urlpatters = [
# ...
path(
'api/v1/auth/2factor/',
include(
(twofactor_make_urlpatterns(), 'wcd_2factor'),
namespace='2factor',
)
),
]
```
And after the `/api/v1/auth/2factor/` you will have several endpoints:
#### Method configurations
**GET:** `method-config/list/active/` - List of active method configs.
#### User configurations
**GET:** `user-config/own/list/` - List of user's configurations.
**POST:** `user-config/own/create/` - Creating a new user configuration.
```json
{
// Selected global method config.
"method_config_id": 1,
// Configuration data.
"config": {"email": "ad@ad.com"},
// 2Factor method could be deactivated by user.
"is_active": false,
// Setting some method as a default.
"is_default": false,
}
```
**POST:** `user-config/own/confirm/` - Confirming unconfirmed user configuration.
```json
{
// User config id.
"id": 1,
// Unconfirmed confirmation identifier.
"confirmation_id": "uuid-confirmation-identifier-0000",
// Data to confirm with.
"data": {"code": "some"},
}
```
**PUT:** `user-config/own/<int:pk>/update/` - Updating user configuration.
```json
{
// Configuration data.
"config": {"email": "ad@ad.com"},
// 2Factor method could be deactivated by user.
"is_active": false,
// Setting some method as a default.
"is_default": false,
}
```
**DELETE:** `user-config/own/<int:pk>/destroy/` - Deletes user configuration.
#### Confirmation
**POST:** `confirmation/request/` - Creating a new confirmation.
```json
{
// One of `method_config_id` or `user_config_id` must be provided:
// Selected global method config. For example if user doesn't have it's own.
"method_config_id": 1,
// Selected user configuration method.
"user_config_id": 1,
// Additional data, if required. For example an "email" to confirm.
"data": {"some": "data"},
}
```
Result will have a confirmation identifier. So on the frontend side it should be saved to confirm later.
**GET:** `confirmation/{confirmation_id}/check/` - Will return current `ConfirmationState` status.
**POST:** `confirmation/confirm/` - Method to confirm previously created "request".
```json
{
// ConfirmationState identifier.
"id": "uuid-confirmation-identifier-0000",
// User data, that backend will use to validate the confirmation.
"data": {"code": "confirmation-000-code"},
}
```
It will return the same data as previous requests, but this time confirmation `status` will be `confirmed`, or not if confirmation failed.
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.0]
### Changed
- Total rewrite. See docs.
## [0.1.7]
### Added
- Default to confirmation states admin list.
- New django unified `JSONField` support.
## [0.1.6]
### Added
- Translation strings.
## [0.1.3]
### Added
- Admin search ui for confirmation state model.
## [0.1.1]
### Added
- `DEBUG_CODE_RESPONSE` setting. It adds generated 'code' field to a request confirmation response for easier debug.
## [0.1.0]
Initial version.
Raw data
{
"_id": null,
"home_page": null,
"name": "wc-django-2factor",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.6",
"maintainer_email": null,
"keywords": null,
"author": "WebCase",
"author_email": "info@webcase.studio",
"download_url": "https://files.pythonhosted.org/packages/27/d1/4b70a2029c3c6b472cb3599805a168ae3ab63dc395f43dee48c977020883/wc_django_2factor-0.2.1.tar.gz",
"platform": null,
"description": "# WebCase 2factor API\n\nPackage to create general API for 2factor checkers.\n\n## Installation\n\n```sh\npip install wc-django-2factor\n```\n\nIn `settings.py`:\n\n```python\nINSTALLED_APPS += [\n # Dependencies:\n 'pxd_admin_extensions',\n 'django_jsonform',\n 'wcd_settings',\n\n # 2Factor itself.\n 'wcd_2factor',\n]\n\nWCD_2FACTOR = {\n # It will be empty by default:\n 'METHODS': [\n # Simple builtin 2factor method.\n # Used to work like user secret confirmation.\n # But mostly serves as an example.\n 'wcd_2factor.builtins.dummy.DUMMY_METHOD_DESCRIPTOR',\n ],\n # Custom json encoder\n 'JSON_ENCODER': 'wcd_2factor.utils.types.EnvoyerJSONEncoder',\n}\n```\n\n## Usage\n\n### Confirmer\n\nService for confirmation state management.\n\n```python\nfrom wcd_2factor.confirmer import default_confirmer, Backend, Confirmer\nfrom wcd_2factor.registries import method_config_registry\nfrom wcd_2factor.models import MethodConfig, UserConfig, ConfirmationState\n\n# Default registry\n\n# Use:\ndefault confirmer\n# Or create another.\nconfirmer = Confirmer(method_config_registry)\n\n# List of all available method keys:\ndefault_confirmer.get_methods()\n\n# List of all active `MethodConfig` instances:\ndefault_confirmer.get_method_configs()\n\n# List of all user `UserConfig` configurations:\ndefault_confirmer.get_user_configs(\n # Provide user instance:\n user=user or None,\n # Or identifier:\n user_id=user.pk or None\n)\n\n\n# Creates backend for some method config or `None` if there is no such:\ndefault_confirmer.make_backend(\n # Optional, if `user_config` will be provided, since it also has a\n # relation to a MethodConfig.\n method_config=MethodConfig() or None,\n # Optional, since confirmation could be done just using the MethodConfig \n # by itself.\n user_config=UserConfig() or None,\n # Optional context to be passed to the backend.\n context={} or None,\n # Whether should raise an exception if backend could not be created.\n # For example when there is no registered method.\n should_raise=False,\n)\n\n\n# Method to change user confirmation.\n# Id will check if the changes are significant enough to request a \n# confirmation from user.\n# If it does - `make_confirmation` - will be a callable to create new \n# `ConfirmationState` instance. Else `None` will be returned.\ninstance = UserConfig()\nmake_confirmation = default_confirmer.change_user_config(\n # Current instance to apply changes to.\n instance,\n # New configuration object. Either a pydantic object or dataclass or just a \n # simple dict, that will be internally converted to a pydantic object.\n DTO() or dict(),\n # If you already have an initialized backend, method could use it \n # instead of creating a new one:\n backend=Backend() or None,\n # Optional method config object. If, for example `user_config` instance \n # doesen't have one attached yet,\n method_config=MethodConfig() or None,\n # Optional context to be passed to the backend's method.\n context={} or None,\n)\n# Don't forget to save your configuration instance.\n# It will not be saved by this method.\ninstance.save()\n\nif make_confirmation is not None:\n confirmation: ConfirmationState = make_confirmation()\n\n \n# Requesting any type of confirmation:\nconfirmation: ConfirmationState = default_confirmer.request_confirmation(\n # Optional, if `user_config` will be provided, since it also has a\n # relation to a MethodConfig.\n method_config=MethodConfig() or None,\n # Optional, since confirmation could be done just using the MethodConfig \n # by itself.\n user_config=UserConfig() or None,\n # If you already have an initialized backend, method could use it \n # instead of creating a new one:\n backend=Backend() or None,\n # User provided state.\n # It depends on backend what kind of parameters should and should not be \n # present.\n # In most cases if `used_config` provided - no additional information \n # required at all.\n state={} or None,\n # Optional context to be passed to the backend's method.\n context={} or None,\n)\n\n\n# If you have user data to confirm some `ConfirmationState` run this:\nconfirmation: ConfirmationState = default_confirmer.confirm(\n # Either identifier:\n id=uuid4() or None,\n # Or confirmation object itself must be provided:\n confirmation=confirmation or None,\n # User passed data, that confirms that user have control over the \n # \"second factor\":\n data={} or None,\n # If you already have an initialized backend, method could use it \n # instead of creating a new one:\n backend=Backend() or None,\n # Optional context to be passed to the backend's method.\n context={} or None,\n)\n# Method might return state, event when confirmation process failed for some \n# reason.\n# So check the confirmation before using it:\nif not confirmation.is_available():\n raise ValueError('Confirmation failed.')\n\n\n# Checks if confirmation is confirmed and available to use:\navailable, optional_confirmation = default_confirmer.check(\n # Either identifier:\n id=uuid4() or None,\n # Or confirmation object itself must be provided:\n confirmation=confirmation or None,\n # Optional context to be passed to the backend's method.\n context={} or None,\n)\n# In some cases method might return None instead of confirmation object.\n# That happens when confirmation was already used, or there were none at all.\nif not available:\n raise ValueError('Confirmation unavailable.')\n\n\n# And the last one.\n# ConfirmationState object is a \"one-time\" password to perform some action.\n# So after usage it will be deleted from the database.\nused, optional_confirmation = default_confirmer.use(\n # Either identifier:\n id=uuid4() or None,\n # Or confirmation object itself must be provided:\n confirmation=confirmation or None,\n # Optional context to be passed to the backend's method.\n context={} or None,\n)\n# But you will still have object returned if `used` was true.\n# You might need to do something with it afterwards.\nif not used:\n raise ValueError('Confirmation failed.')\n```\n\n### Registry and custom Backends\n\nRegistry is a simple dict with some additional methods to register new confirmation methods.\n\nFor every method that could be used in your application `MethodConfigDescriptor` should be defined and added to registry.\n\nFor example:\n\n```python\nfrom wcd_2factor.registries import (\n method_config_registry, Registry,\n MethodConfigDescriptor, DTO,\n)\n\n# This is a default method's registry. \n# It will be autopopulated with descriptors from \n# django_settings.WCD_2FACTOR['METHODS'].\nmethod_config_registry\n\n# But nothing stops you from creating your own registry.\nmy_registry = Registry()\n\n# And after that you may add descriptors to it.\nMY_METHOD_DESCRIPTOR = my_registry.register(MethodConfigDescriptor(\n # Unique method key.\n key='my_method',\n # Verbose method name.\n verbose_name=gettext_lazy('My Method'),\n # Backend class is required, since it will be used to execute every\n # `Confirmer` method.\n backend_class=Backend,\n # Other data object classes and schemas are optional:\n # MethodConfig pydantic class.\n # Configuration model for MethodConfig.\n config_global_dto=BaseModel or None,\n # JSONSchema for that configuration.\n config_global_schema=BaseModel.model_json_schema() or None,\n # Configuration model for UserConfig.\n config_user_dto=BaseModel or None,\n # JSONSchema for that configuration.\n config_user_schema=BaseModel.model_json_schema() or None,\n))\n```\n\nBut descriptor is only a simple definition with and additional configuration inside.\n\nAll the work with message sending and request confirmation are on your `Backend` implementation.\n\n```python\nfrom wcd_2factor.confirmer import Backend\nfrom wcd_2factor.registries import DTO, MethodConfigDescriptor\nfrom wcd_2factor.models import ConfirmationState, UserConfig\n\n\nclass YourBackend(Backend):\n method_config: YourMethodDTO\n user_config: Optional[YourUserDTO]\n\n # Method that checks if user configuration changed.\n # And if this change is significant enough to request a confirmation.\n def change_user_config(\n self,\n # New configuration to check for changes.\n new: YourMethodDTO,\n context: Optional[dict] = None,\n ) -> Tuple[bool, Optional[dict]]:\n # Pseudocode:\n\n if (\n self.user_config is None\n or\n self.user_config != new\n ):\n # Then user configuration changed and confirmation with\n # some \"state\" should be created to confirm the change.\n return True, {'some': 'state'}\n\n # Otherwise - do nothing.\n return False, None\n\n # This is method for all confirmation requests creation.\n # Whether it's for user confirmation or not, with empty `self.user_config` \n # and only `self.method_config` available or \"fully configured\"\".\n def request_confirmation(\n self,\n # User or application provided state.\n state: dict,\n context: Optional[dict] = None,\n ) -> Tuple[ConfirmationState.Status, dict]:\n return ConfirmationState.Status.IN_PROCESS, {\n **state,\n 'some_confirmation_token_to_check': 'value',\n }\n\n # To confirm saved confirmation state, `Confirmer` will call this method.\n def confirm(\n self,\n # State from `ConfirmationState` object.\n state: dict,\n # User-provided data to validate against.\n user_data: Any,\n context: Optional[dict] = None,\n ) -> bool:\n # Return True if user provided something that is somehow valid \n # against the stored state.\n # User will never have access to the ConfigurationState data from \n # your `request_confirmation` object.\n # At least he should not.\n return (\n state.get('some_confirmation_token_to_check')\n ==\n user_data.get('validation_token')\n )\n```\n\n### Frontend/DRF\n\nLibrary has an API implmentation based on DjangoRestFramework.\n\nIt is available in `wcd_2factor.contrib.dtf` module.\n\nIn `urls.py`:\n\n```python\nfrom wcd_2factor.contrib.drf.views import make_urlpatterns as twofactor_make_urlpatterns\n\n\nurlpatters = [\n # ...\n path(\n 'api/v1/auth/2factor/',\n include(\n (twofactor_make_urlpatterns(), 'wcd_2factor'),\n namespace='2factor',\n )\n ),\n]\n```\n\nAnd after the `/api/v1/auth/2factor/` you will have several endpoints:\n\n#### Method configurations\n\n**GET:** `method-config/list/active/` - List of active method configs.\n\n#### User configurations\n\n**GET:** `user-config/own/list/` - List of user's configurations.\n\n**POST:** `user-config/own/create/` - Creating a new user configuration.\n```json\n{\n // Selected global method config.\n \"method_config_id\": 1,\n // Configuration data. \n \"config\": {\"email\": \"ad@ad.com\"},\n // 2Factor method could be deactivated by user.\n \"is_active\": false,\n // Setting some method as a default.\n \"is_default\": false,\n}\n```\n\n**POST:** `user-config/own/confirm/` - Confirming unconfirmed user configuration.\n```json\n{\n // User config id.\n \"id\": 1,\n // Unconfirmed confirmation identifier.\n \"confirmation_id\": \"uuid-confirmation-identifier-0000\",\n // Data to confirm with.\n \"data\": {\"code\": \"some\"},\n}\n```\n\n**PUT:** `user-config/own/<int:pk>/update/` - Updating user configuration.\n```json\n{\n // Configuration data. \n \"config\": {\"email\": \"ad@ad.com\"},\n // 2Factor method could be deactivated by user.\n \"is_active\": false,\n // Setting some method as a default.\n \"is_default\": false,\n}\n```\n\n**DELETE:** `user-config/own/<int:pk>/destroy/` - Deletes user configuration.\n\n#### Confirmation\n\n**POST:** `confirmation/request/` - Creating a new confirmation.\n```json\n{\n // One of `method_config_id` or `user_config_id` must be provided:\n // Selected global method config. For example if user doesn't have it's own.\n \"method_config_id\": 1,\n // Selected user configuration method.\n \"user_config_id\": 1,\n // Additional data, if required. For example an \"email\" to confirm.\n \"data\": {\"some\": \"data\"},\n}\n```\n\nResult will have a confirmation identifier. So on the frontend side it should be saved to confirm later.\n\n**GET:** `confirmation/{confirmation_id}/check/` - Will return current `ConfirmationState` status.\n\n**POST:** `confirmation/confirm/` - Method to confirm previously created \"request\".\n```json\n{\n // ConfirmationState identifier.\n \"id\": \"uuid-confirmation-identifier-0000\",\n // User data, that backend will use to validate the confirmation.\n \"data\": {\"code\": \"confirmation-000-code\"},\n}\n```\n\nIt will return the same data as previous requests, but this time confirmation `status` will be `confirmed`, or not if confirmation failed.\n# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.2.0]\n### Changed\n- Total rewrite. See docs.\n\n## [0.1.7]\n### Added\n- Default to confirmation states admin list.\n- New django unified `JSONField` support.\n\n## [0.1.6]\n### Added\n- Translation strings.\n\n## [0.1.3]\n### Added\n- Admin search ui for confirmation state model.\n\n## [0.1.1]\n### Added\n- `DEBUG_CODE_RESPONSE` setting. It adds generated 'code' field to a request confirmation response for easier debug.\n\n## [0.1.0]\nInitial version.\n",
"bugtrack_url": null,
"license": "MIT License",
"summary": "Package to create general API for 2factor checkers.",
"version": "0.2.1",
"project_urls": null,
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "27d14b70a2029c3c6b472cb3599805a168ae3ab63dc395f43dee48c977020883",
"md5": "58ecd73222847e9c5494821b8f73311e",
"sha256": "3417f2a3d1b4472beee88767a28b093cd9cf81f6b483464dccd6e1a239686ffb"
},
"downloads": -1,
"filename": "wc_django_2factor-0.2.1.tar.gz",
"has_sig": false,
"md5_digest": "58ecd73222847e9c5494821b8f73311e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.6",
"size": 43538,
"upload_time": "2024-12-05T13:43:55",
"upload_time_iso_8601": "2024-12-05T13:43:55.724792Z",
"url": "https://files.pythonhosted.org/packages/27/d1/4b70a2029c3c6b472cb3599805a168ae3ab63dc395f43dee48c977020883/wc_django_2factor-0.2.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-05 13:43:55",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "wc-django-2factor"
}