django-auth-recovery-codes 1.0.3
Django app for 2FA recovery codes
License: MIT
Requires Python: <3.14,>=3.10

![Made with Python](https://img.shields.io/badge/Made%20with-Python-blue?logo=python)
![Security](https://img.shields.io/badge/Security-213--bit-brightgreen)
![Brute Force](https://img.shields.io/badge/Brute--force-Impractical-red)
![License](https://img.shields.io/badge/License-MIT-yellow)
[![Downloads](https://static.pepy.tech/badge/django-auth-recovery-codes)](https://pepy.tech/project/django-auth-recovery-codes)



## 🔐 Django 2FA Recovery Codes

The premises of this resuable application, is that it takes any Django application and extends that application so that it can now use the 2FA recovery codes as a backup login should you lose access.

`django_auth_recovery_codes` is a Django app that provides a robust system for generating, storing, and managing **2FA recovery codes**. Unlike a full two-factor authentication apps, this package focuses solely on **recovery codes**, although this is a lightweight application it is a very powerful tool, offering fine-grained control and asynchronous management for better UX and performance.

## Table of Contents
* [Requirements & Key Technologies](#requirements--key-technologies)
* [Key Features](#key-features)
* [Quickstart video walkthrough](#quickstart-video-walkthrough)
* [How it Differs From A Full Two-Factor Authentication Apps](#how-it-differs-from-a-full-two-factor-authentication-apps)
  * [User Interface](#user-ui-interface)
  * [Asynchronous usage](#asynchronous-usage)
  * [Admin interface](#admin-interface)
  * [Flag configuration](#flag-configuration)
  * [Caching](#caching)
  * [Email and Logging capabilities](#email-and-logging-capabilities)
  * [Rate limiter](#rate-limiter)
  * [Code generation attribrutes](#code-generation-attribrutes)
    * [Cleanup Configuration Examples](#cleanup-configuration-examples)
    * [Large app (millions of codes)](#large-app-millions-of-codes)
    * [Cleanup Process Visualised](#cleanup-process-visualised)
    * [Recommended Settings by Scale](#recommended-settings-by-scale)
    * [How Cleanup Works Internally](#how-cleanup-works-internally)
    * [FAQ Frequently Asked Questions](#faq--frequently-asked-questions)
  * [Configurable flags for developer](#configurable-flags-for-developer)
* [2FA Recovery Code Generator](#django-2fa-recovery-code-generator)
  * [Security overview](#security-overview)
  * [Entropy](#entropy)
  * [Total Combinations](#total-combinations)
  * [Brute-Force Resistance](#brute-force-resistance)
  * [Perspective](#prespective)
  * [Developer Appendix 🛠️](#developer-appendix)
  * [Summary](#summary)
  * [Use Cases](#use-cases)
* [Installation](#installation)
* [Quick Example](#quick-example) 
* [How to Use 2FA Recovery Codes](#how-to-use-2fa-recovery-codes)
  * [Setting up the Cache or using the default cache](#setting-up-the-cache-or-using-the-default-cache)
  * [How does the cache work?](#how-does-the-cache-work)
  * [Using Pagination and Caching](#using-pagination-and-caching)
  * [What cache should I use?](#what-cache-should-i-use)
  * [Using Django Cache Without Configuring a Backend](#using-django-cache-without-configuring-a-backend)
  * [Django-Q vs Celery and why Django Auth Recovery codes use Django-q](#django-q-vs-celery-and-why-django-auth-recovery-codes-use-django-q)
  * [Why this application uses Django-Q](#why-this-application-uses-django-q)
  * [Using Django-Q with `django_auth_recovery_codes`](#using-django-q-with-django-2fa-recovery-codes)
  * [Benefits of using Django-Q](#benefits-of-using-django-q)
  * [Setting up Django-q](#setting-up-django-q)
* [Django Auth Recovery Settings](#django-auth-recovery-settings)  
* [Using and setting up Django-q](#using-and-setting-up-django-q)
* [Django Auth Recovery flag settings](#django-auth-recovery-flag-settings)
  * [Recovery Code Display Settings](#recovery-code-display-settings)
  * [Cooldown Settings Flags](#cooldown-settings-flags)
  * [Rate Limiting & Caching](#rate-limiting--caching)
  * [Audit & Logging Setting Flags](#audit--logging-setting-flags)
  * [Email & Admin Settings Flags](#email--admin-settings-flags)
  * [Code Management & Limits](#code-management--limits)
  * [Site Settings Flags](#site-settings-flags)
  * [Example Flag Usage](#example-flag-usage)
  * [Best Practices for Managing Environment Variables](#best-practices-for-managing-environment-variables)
  * [Default Values & Required Variables](#default-values--required-variables)
  * [Run checks to verify that flags are valid](#run-checks-to-verify-that-flags-are-valid)
* [Sending Emails and using Logging](#sending-emails-and-using-logging)
  * [Using async vs synchronous](#using-async-vs-synchronous)
  * [Configuration settings](#configuration-settings)
  * [Hang on a minute, why can I email myself the code only once, and only if I haven’t logged out after generating it?](#hang-on-a-minute-why-can-i-email-myself-the-code-only-once-and-only-if-i-havent-logged-out-after-generating-it)
  * [What does this mean for your codes?](#what-does-this-mean-for-your-codes)
  * [What happens if I refresh the page, can I still email myself the code?](#what-happens-if-i-refresh-the-page-can-i-still-email-myself-the-code)
  * [But if I’m still logged in, why can I only email myself a single copy?](#but-if-im-still-logged-in-why-can-i-only-email-myself-a-single-copy)
  * [Can I email myself a copy if I generate a new batch?](#can-i-email-myself-a-copy-if-i-generate-a-new-batch)
* [Using Logging with the Application](#using-logging-with-the-application)
  * [What if I don’t want to override my existing LOGGING configuration?](#what-if-i-dont-want-to-override-my-existing-logging-configuration)
* [Downloading Recovery Codes](#downloading-recovery-codes)
  * [How downloads work](#how-downloads-work)
  * [Important security notes](#important-security-notes)
  * [Example usage](#example-usage)
* [Quickstart and Walkthrough read](#quickstart-and-walkthrough)
  * [Setup](#setup)
  * [Installation (with Virtual Environment)](#installation-with-virtual-environment)
  * [3. Upgrade pip (optional but recommended)](#3-upgrade-pip-optional-but-recommended)
  * [4. Install Django (latest version)](#4-install-django-latest-version)
  * [5. Install the recovery codes package](#5-install-the-recovery-codes-package)
  * [6. Verify installation](#6-verify-installation)
  * [8. Run initial migrations](#8-run-initial-migrations)
  * [9. Create a Django superuser](#9-create-a-django-superuser)
  * [10. Start a new app called `home`](#10-start-a-new-app-called-home)
  * [12. Run the development server](#12-run-the-development-server)
  * [Configure URLs](#configure-urls)
  * [Configure your Settings.py file](#configure-your-settingspy-file)
  * [16. Set up the file-based email backend (for testing)](#16-set-up-the-file-based-email-backend-for-testing)
  * [17. Run the system checks](#17-run-the-system-checks)
  * [17a. Generate a recovery code](#17a-generate-a-recovery-code)
  * [Run Services](#run-services)
  * [Create a Home View](#create-a-home-view)
  * [Access the Admin](#access-the-admin)
  * [Access the Recovery Codes page dashboard](#access-the-recovery-codes-page-dashboard)
  * [Code Generation](#code-generation)
  * [Verifying Generated Codes](#verifying-generated-codes)
  * [Downloaded and Emailed Code](#downloaded-and-emailed-code)
  * [Invalidating or Deleting a Code](#invalidating-or-deleting-a-code)
  * [Viewing the Code Batch History](#viewing-the-code-batch-history)
  * [Logout of the application](#logout-of-the-application)
  * [Failed Attempts and Rate Limiting](#failed-attempts-and-rate-limiting)
  * [Successful Login](#successful-login)
  * [Existing Project Setup](#2-existing-project-setup)
  * [Scheduling a Code Removal Using Django-Q](#scheduling-a-code-removal-using-django-q)
  * [Running Tests](#running-tests)
  * [Warning ⚠](#warning)
* [Known Issues](#known-issues)
* [License](#license)
* [Support](#support)


---

## Requirements & Key Technologies

- **Python 3.10+**  
  This library uses [Structural Pattern Matching](https://docs.python.org/3/whatsnew/3.10.html#structural-pattern-matching), introduced in Python 3.10, via the `match`-`case` syntax.

- **Django 5.2+**  
  The project is built and tested using Django 5.2, which has Long-Term Support (LTS).  
  Using an earlier version, such as Django 4.1.x, may work in some cases but is **not guaranteed** and could potentially break the application, since it has only been tested with Django 5.2.

### Why Python 3.10?

The project relies on the `match`-`case` syntax, which provides a more readable and expressive way to handle complex conditional logic.

## Key Technologies and Libraries Used

- **EmailSender**: a lightweight but powerful library for sending chainable rich emails with templates. [Find out more](https://github.com/EgbieAndersonUku1/django-email-sender)
- **EmailSenderLogger**: a lightweight but powerful library for logging, emails sent by EmailSender. [Find out more](https://github.com/EgbieAndersonUku1/django-email-sender)
- **Django-q**: an asynchronous task manager used for processing background tasks, including using EmailSender to send emails.  
- **JavaScript (JS)**: for interactivity and fetch requests.  
- **HTML**: for structuring content.  
- **CSS**: for styling the user interface.

---

---


### Key Features

* Generate recovery codes in configurable batches.
* Track recovery codes individually:
  * Mark codes as used, inactive, or scheduled for deletion.
  * User the 2FA code to login which becomes invalid after a single use
* Batch management:
  * Track issued and removed codes per batch.
  * Statuses for active, invalidated, or deleted batches.
* Login
  * Login in using your 2FA Recovery Backup code
* Asynchronous cleanup using Django-Q:
  * Delete expired or invalid codes without locking the database.
  * Scheduler allows admins to set cleanup intervals (e.g., every 2 days) without touching code.
  * Optional options to email the report to the admin
  * Optional option to store user emails (Whenever the user send themselves a code) in the database
  * Optional scheduler to delete Recovery code Audit model (tracks the users, the number of code issued, time issued, etc)
* Secure storage:
  * Codes are hashed before saving; no plaintext storage.
* Extensible utilities for generating and verifying codes.

---

### How It Differs From A Full Two-Factor Authentication Apps?

`django_auth_recovery_codes` is designed **solely for recovery codes**, offering fine-grained control, asynchronous management, and admin-friendly batch handling.

* ### User UI interface
   * Dedicated login interface page to enter your email and 2FA recovery code
   * Dashboard that allows the user to:
	      * Generate a batch of 2FA recovery codes (default=10 generated, configurable via settings flags) with expiry date or doesn't expiry
        * Regenerate code (Uses brute force rate limiter with a penalty that increases the wait time if codes is regenerated within that time window)
        * Email, Delete or Download entire codes via the buttons
        * One-time verification code setup form
          * A one-time setup that allows the user to enter a 2FA code after generation (for the first time) to verify that the backend has configured it correctly without marking the code as used. The tests indicate whether the code has been set up correctly.

        * Invalidate or delete a code via interactive form
        * view batch histories
	
          #### Example a single recovery code batch View

          | Field                     | Value                                |
          | ------------------------- | ------------------------------------ |
          | Batch ID                  | 8C2655A1-8F14-4B56-AEC8-7DDA72F887A4 |
          | Expiry info               | Active                               |
          | User                      | Egbie                                |
          | Date issued               | 23 Sept. 2025,                  |
          | Date modified             | 23 Sept. 2025,                |
          | Number of codes issued    | 10                                   |
          | Number of codes used      | 0                                    |
          | Number of deactivated     | 0                                    |
          | Number of removed         | 0                                    |
          | Has generated code batch  | True                                 |
          | Has viewed code batch     | True                                 |
          | Has downloaded code batch | False                                |
          | Has emailed code batch    | False                                |

      * Pagination to split the batch recovery codes history on different pages instead of one long page

* Focuses **exclusively on recovery codes**, rather than full 2FA flows.
* Built-in **logger configuration** which can be imported into settings or merged with an existing logger.

* ### Asynchronous Usage

  * Built with **asynchronous usage** using Django-Q:
  * Automatically deletes expired or invalid codes when uses with scheduler.
  * On a successful delete scheduler generates an audit report of the number of deleted codes and sends it to admin via email.
  * Email sending can be configured to run **asynchronous or synchronous** depending on your environment:
    * `DEBUG = True` : uses synchronous sending (easy for development or testing).  
    * `DEBUG = False` : uses asynchronous sending (recommended for production; doesn’t block the application while sending in the background).

* ### Admin interface
  * **Admin-friendly view interface code management**, including the ability to scheduler deletion for expired or invalid codes e.g (every 2 days, etc) or even the audit history.
* ### Code tracking
  * **Individual code tracking** with granular control over each code.
* ### Flag configuration
  * Optional configuration to  turn **logger** on or off to track the actions of users generating recovery codes, email sent, various aspect of the models, etc.
  * Optional **storage of user email** in the model for auditing purposes.
* ### Caching
  * Utilises **caching** (Redis, Memcached, default cache, etc) for
    * Pagination and page reads
    * Brute-force rate limiting
    * Other database-heavy operations
    * Reduces database hits until cache expires or updates are made.

* ### Email and logging capabilities
  * **Email sending capabilities** via `EmailSender` library.
  * **Email logging** via `EmailSenderLogger` library.

* ### Rate limiter
  * **Maximum login attempt control** with a brute-force rate limiter:  
    * Configurable penalty wait times that increase if a user retries during the wait window.
  * **Brute-force rate limiter** for code generation:
    * Prevents spam and imposes a penalty if the user attempts regeneration too soon.

* ### Code generation attribrutes
  * Generate **codes that expire** or have no expiry.
  * Allow users to **download or email codes** (one per batch).
  * **Invalidate, delete a single code or an entire batch** easily.
  * Users can **view batch details**, e.g., number of codes generated, emailed, or downloaded.

* ### Configurable flags for developer

     #### Configuration flags settings for the Django Auth Recovery code app

      ```python
      DJANGO_AUTH_RECOVERY_CODES_ADMIN_SENDER_EMAIL = 
      DJANGO_AUTH_RECOVERY_CODE_ADMIN_EMAIL_HOST_USER = 
      DJANGO_AUTH_RECOVERY_CODE_ADMIN_USERNAME = 
      DJANGO_AUTH_RECOVERY_CODE_STORE_EMAIL_LOG = 

      DJANGO_AUTH_RECOVERY_KEY = 

      DJANGO_AUTH_RECOVERY_CODE_AUDIT_ENABLE_AUTO_CLEANUP = 
      DJANGO_AUTH_RECOVERY_CODE_AUDIT_RETENTION_DAYS = 
      DJANGO_AUTH_RECOVERY_CODE_PURGE_DELETE_RETENTION_DAYS = 
      DJANGO_AUTH_RECOVERY_CODE_PURGE_DELETE_SCHEDULER_USE_LOGGER = 

      DJANGO_AUTH_RECOVERY_CODES_AUTH_RATE_LIMITER_USE_CACHE = 
      DJANGO_AUTH_RECOVERY_CODES_BASE_COOLDOWN = 
      DJANGO_AUTH_RECOVERY_CODES_COOLDOWN_CUTOFF_POINT = 
      DJANGO_AUTH_RECOVERY_CODES_COOLDOWN_MULTIPLIER = 
      DJANGO_AUTH_RECOVERY_CODES_MAX_LOGIN_ATTEMPTS = 

      DJANGO_AUTH_RECOVERY_CODES_CACHE_MAX = 
      DJANGO_AUTH_RECOVERY_CODES_CACHE_MIN = 
      DJANGO_AUTH_RECOVERY_CODES_CACHE_TTL = 

      DJANGO_AUTH_RECOVERY_CODE_MAX_VISIBLE = 
      DJANGO_AUTH_RECOVERY_CODE_PER_PAGE = 
      DJANGO_AUTH_RECOVERY_CODES_BATCH_DELETE_SIZE = 
      DJANGO_AUTH_RECOVERY_CODES_MAX_DELETIONS_PER_RUN = 

      DJANGO_AUTH_RECOVERY_CODES_DEFAULT_FILE_NAME = 
      DJANGO_AUTH_RECOVERY_CODES_DEFAULT_FORMAT = 

      DJANGO_AUTH_RECOVERY_CODES_SITE_NAME = 
      DJANGO_AUTH_RECOVERY_CODE_REDIRECT_VIEW_AFTER_LOGOUT = 

      DJANGO_AUTH_RECOVERY_CODE_EMAIL_SUCCESS_MSG = 

       
      ```
[⬆ Back to Top](#top)

--- 

## Django 2FA Recovery Code Generator

### **Security Overview**

These 2FA recovery codes generated are designed to be **extremely secure** and practically impossible to guess. Protects against Brute force, Rainbow attacks and timed attacks

### **Code Format**

```
XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX
```

* **6 groups** of **6 characters** each (36 characters)
* **Alphabet:** 60 characters (`A–Z`, `a–z`, `2–9`), the app avoiding confusing characters like `0` vs `O` and `1` vs `l`
* **Cryptographically secure randomness** ensures codes are unpredictable

---

### **Entropy**

Entropy measures how unpredictable a code is, the higher the entropy, the harder it is to guess.

* **Entropy per character:**

$$
\log_2(60) \approx 5.91 \text{ bits}
$$

* **Entropy per group (6 characters):**

$$
6 \times 5.91 \approx 35.5 \text{ bits}
$$

* **Total entropy for the full 36-character code:**

$$
36 \times 5.91 \approx 213 \text{ bits}
$$

> For comparison, AES-128 encryption has 128 bits of entropy. These recovery codes are **much stronger** in terms of guessing resistance.

---

### **Total Combinations**

With 36 characters chosen from 60 possibilities each:

$$
60^{36} \approx 2 \times 10^{63} \text{ unique codes}
$$

This astronomical number of possibilities ensures that **guessing a valid code is virtually impossible**.

---

### **Brute-Force Resistance**

Even with a supercomputer that tests codes extremely quickly, brute-forcing a valid recovery code is **completely impractical**:

| Attack Speed              | Seconds   | Years     |
| ------------------------- | --------- | --------- |
| 1 billion/sec (10^9)      | 2 × 10^54 | 6 × 10^46 |
| 1 trillion/sec (10^12)    | 2 × 10^51 | 6 × 10^43 |
| 1 quintillion/sec (10^18) | 2 × 10^45 | 6 × 10^37 |

> **For comparison:** the age of the universe is only \~1.4 × 10^10 years. Even a computer testing a **quintillion codes per second** would need far longer than the universe has existed to find a valid code.

---

### Prespective?

### What this means?

* Each character is chosen randomly from 60 possibilities.
* With 36 characters, the number of possible codes is **more than 2 followed by 63 zeros**.
* Each recovery code has **≈213 bits of entropy**, making it **extremely resistant to guessing or brute-force attacks**.
* That’s **so many possibilities** that even the fastest computers would take **longer than the age of the universe** to try them all.
* The vast number of possible codes ensures that **every code is unique and unpredictable**.
* This makes guessing a valid code virtually impossible and this is without brute rate limiter, with a rate limiter (which this app uses it is virtually impossible).

> In short: it’s **far stronger than standard encryption like AES-128**. 

<div align="center">
  <img src="django_auth_recovery_codes/docs/images/security-strength.png" alt="2FA login form" width="400">
</div>

> You can trust these recovery codes to keep your account safe even against attackers with enormous computational power.

[⬆ Back to Top](#top)

---

### Developer Appendix

```python
import math

def brute_force_time(alphabet_size=52, chars_per_group=6, groups=6, guesses_per_second=10**9):
    total_combinations = alphabet_size ** (chars_per_group * groups)
    seconds = total_combinations / guesses_per_second
    years = seconds / (60 * 60 * 24 * 365)
    return total_combinations, seconds, years

combos, seconds, years = brute_force_time()
print(f"Total combinations: {combos:e}")
print(f"Seconds to crack: {seconds:e}")
print(f"Years to crack: {years:e}")
```

**Example output:**

```
Total combinations: 3.292e+61
Seconds to crack: 3.292e+52
Years to crack: 1.043e+45
```

---

#### Summary

* **212.8 bits recovery codes** → astronomically secure
* **≈3.3 × 10^61 combinations** → impossible to brute-force
* Even with a supercomputer, cracking a single code would take **trillions of times longer than the age of the universe**
* With **rate limiting**, brute-force becomes completely infeasible

[⬆ Back to Top](#top)

---

#### Use Cases

* Integrate with any existing 2FA system to provide a secure set of recovery codes.
* Large-scale systems where thousands of users might need recovery codes, ensuring database performance is not impacted.
* Admin-friendly management of recovery codes, including scheduling cleanups without developer intervention.
* Systems requiring secure, hashed storage of recovery codes while retaining full control over their lifecycle.

[⬆ Back to Top](#top)

---

## Installation

```bash
pip install django_auth_recovery_codes
```

```python
# settings.py
INSTALLED_APPS = [
    ...
    'django_2fa_recovery_codes',
    'django_q',
]
```

---

## Quick Example

```python
from django_2fa_recovery_codes.models import RecoveryCodeBatch

# Create a batch of 10 recovery codes for a user
plain_codes, batch_instance = RecoveryCodeBatch.create_recovery_batch(user, days_to_expire=30)


```

---


## How to Use 2FA Recovery Codes

### Setting up the Cache or using the default cache

To use this application, you can either set up a permanent cache system in the backend or allow it to use the default cache.

### Why is a cache necessary for this app?

This application is designed to be scalable, meaning it can support anything from a few users to thousands without compromising performance or putting unnecessary load on the database. It relies heavily on caching: 

  - Everything from page reads
  - Pagination
  - Brute-force rate limiting, waiting time for failed login attempts to the cooling period for regenerating new codes is computed and cached. 
  - Database-heavy operations

The database is only accessed when the cache expires or an update is made e.g the user uses, deletes or invalidates a code.


#### Cache Expiry and TTL Settings

Cache entries have a configurable **Time-To-Live (TTL)**, which determines how long the data is stored before being refreshed. The following settings are used by default:

```python
DJANGO_AUTH_RECOVERY_CODES_CACHE_TTL = 300      # Default 5 minutes
DJANGO_AUTH_RECOVERY_CODES_CACHE_MIN = 60       # Minimum 1 minute
DJANGO_AUTH_RECOVERY_CODES_CACHE_MAX = 3600     # Maximum 1 hour


```

These settings **can be adjusted by the developer** in the Django settings to balance performance with data freshness. This ensures cache expiry times remain within safe and predictable bounds.

### How does the cache work?

The cache helps prevent issues such as **race conditions** and **stale data**.

#### What is a race condition?

A race condition occurs when two or more processes try to modify the same data at the same time, leading to unpredictable results.

**Example:**

Imagine two requests to generate a new 2FA recovery code arrive simultaneously for the same user. If both try to write to the cache at the same time, one could overwrite the other, resulting in lost data. To prevent this, the application ensures that only one process can write to a specific cache key at a time.

This mechanism guarantees that cache data remains consistent, preventing conflicts and ensuring that recovery codes are always valid and reliable.

---


### Using Pagination and Caching

The application uses a **TTL cache** for paginated data. Without caching, every page refresh or navigation triggers a database query to fetch the relevant objects, which can be inefficient.

With the TTL cache (how long an data is cached is determined by the setting flags, default five minutes):

* Paginated query results (e.g., a history of recovery codes) are stored in memory for a set duration.
* While the cache is valid, page refreshes or navigation read from the cache instead of hitting the database.
* When underlying data changes, for example, a new batch of codes is generated, the database and the cache is updated, and the newly updated data from the cache is used. This ensures  that subsequent requests are up-to-date.


### What cache should I use?

That depends entirely on you. The application is designed to **use caching**, but it’s backend-agnostic. It will work with any cache supported by Django (e.g. Redis, Memcached, or in-memory cache). It assumes no cache over the other and leaves it to the developer to decide which one to use under the hood.

This flexibility is possible because the application only interacts with **Django’s cache framework abstraction**. Under the hood, all cache operations (`cache.set`, `cache.get`, etc.) are handled by Django. The actual backend Redis, Memcached, or in-memory is just a plug-in configured in `settings.py`.

* **Redis** : A common choice for production, especially in distributed systems. It supports persistence, clustering, and advanced features like pub/sub.
* **Memcached** : Lightweight and very fast, best for simple key/value caching when persistence is not required.
* **In-memory cache** : Used by default if no backend is configured. Easiest to set up, but limited to a single process and **wipes entirely when the application restarts**, so best for development or small-scale setups.

#### Example configurations (Django)

```python
# settings.py

# Redis
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
    }
}

# Memcached
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
        "LOCATION": "127.0.0.1:11211",
    }
}

# In-memory (local memory cache, default if none configured)
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}
```

#### Example usage

```python
from django.core.cache import cache

# Store a value for 5 minutes
cache.set("greeting", "Hello, world!", timeout=300)

# Retrieve the value
message = cache.get("greeting")
print(message)  # "Hello, world!"
```

### Using Django Cache Without Configuring a Backend

Even if you don’t explicitly define a cache backend in `settings.py`, Django provides a **default in-memory cache (`LocMemCache`)** which the application uses by using the handlers `cache.get()` and `cache.set()` via a specifically designed cache functions:


Key points:

1. Django uses `LocMemCache` internally if `CACHES` is not defined.
2. If a in-memory cache is used (nothing added in the settings) when Django is restarted, the cache is automatically cleared by Django
3. Each worker process has its own separate cache.

---

In short: the app is **built to use caching by default**, but if no backend is configured it automatically falls back to an in-memory cache. However, because it is an in-memory when the Django sever restarts it **resets the cache**. For production, a persistent backend like Redis is recommended.

[⬆ Back to Top](#top)

---

## Using and setting up Django-q

### What is Django-Q?

**Django-Q** is a task queue and asynchronous job manager for Django. It allows your application to run tasks **outside the normal request/response cycle**, this is useful for background processing, scheduling, or parallel execution.

### Key features include:

* Asynchronous Task Execution

Allows tasks to run in the background so users don’t have to wait for them to complete, for example:

* Sending emails
* Generating reports
* Processing files
* Performing API requests
* Deleting tasks

### Scheduled Tasks

Supports scheduling tasks similar to cron jobs:

* One-off tasks at a specific time
* Recurring tasks (daily, weekly, etc.)

### Multiple Brokers

Tasks can be stored in different backends (brokers):

* **Django ORM (default)**: stored in the database
* **Redis**: faster and suitable for high-performance needs
* Other databases (PostgreSQL, MySQL)

### Cluster Mode

Runs multiple worker processes in parallel for better performance and scalability.

### Result Storage

Stores task results so you can check completion status and retrieve outputs.

---

To run the worker cluster, use:

```bash
python manage.py qcluster
```

---

## Django-Q vs Celery and why Django Auth Recovery codes use Django-q


Both Django-Q and Celery are task queues, but they differ in complexity and use cases:

| Feature                   | Django-Q | Celery   |
| ------------------------- | -------- | -------- |
| Async tasks               | ✅        | ✅        |
| Scheduled tasks           | ✅        | ✅        |
| Periodic/recurring tasks  | ✅        | ✅        |
| Multiple brokers          | ✅        | ✅        |
| Result backend            | ✅        | ✅        |
| Retry/failure handling    | Basic    | Advanced |
| Task chaining & workflows | Limited  | ✅        |


**Key differences**:

* **Django-Q** is simpler, uses Django’s ORM as a broker by default, and is ideal for small to medium projects.
* **Celery** is more complex, requires an external broker like Redis or RabbitMQ, and is better suited for large-scale, high-load projects with advanced workflows.

---

## Why does this application use Django-Q?

`django_auth_recovery_codes` uses Django-Q to handle background tasks such as:

1. When the user email themselves a copy of their plaintext code
2. When the admin runs or sets up scheduler (once, daily, weekly, etc) to delete invalid or expired codes, a report is also generated and sent to the admin via email 


Without using Django-q whenever a user deletes their code or sends a copy of their plaintext code it will block normal request/response, and if multiple users are deleting their codes at the same time it can causes problems in the database by. With this it ensures that these tasks do not block normal request/response cycles and can run efficiently in the background without impacting the user experience.


---

### ⚠️ Note on Batch Deletion

Even though expired codes are deleted asynchronously, deleting **millions of codes at once** can still cause performance issues such as long transactions or database locks.

To avoid this, `django_auth_recovery_codes` supports **batch deletion** via the configurable setting:

```python
# settings.py
Django Auth Recovery Codes_BATCH_DELETE_SIZE = 1000
```

* If set, expired codes will be deleted in **chunks of this size** (e.g. 1000 at a time).
* If not set, all expired codes are deleted in a single query.


[⬆ Back to Top](#top)

---


### Using Django-Q with `django_auth_recovery_codes`

Django Auth Recovery Codes provides a utility task to clean up expired recovery codes, but since this is a reusable app, the scheduling of this task is **left up to you**, depending on your project’s needs and dataset size.

---

####. Scheduling the Task with Django-Q via the admin interface `Recovery code batch scheduler`

You can schedule this cleanup task to run at whatever time that that suits via the admin. For example every date at a given time.

See the [Django-Q scheduling docs](https://django-q.readthedocs.io/en/latest/schedules.html) for more options.

---

---

### How does Django-Q delete codes if the user deletes them from the frontend?

`django_auth_recovery_codes` does **not** immediately delete a code when the user deletes it from the frontend. Instead, it performs a **soft delete**, the code is marked as invalid and can no longer be used. From the user’s perspective, the code is “gone,” but the actual row still exists in the database until the cleanup task runs.

When the Django-Q scheduler task runs (either automatically or triggered by the admin), any codes marked for deletion are permanently removed in the background (in batches).

---

### Why not delete the code immediately?

Since this is a **reusable app** that can be plugged into any Django projects of any size (small apps or large-scale environments), immediate deletion is avoided for two key reasons:

1. **Database contention**
   In environments with thousands of users, potential many codes could be deleted at the same time. Deleting them synchronously could lock rows or put heavy strain on the database.

2. **User experience**
   Immediate deletion happens in the request/response cycle. If many users delete codes at once, their requests would take longer, and the frontend might “freeze” while deletions are processed leading to a poor UX.

---

### Benefits of using Django-Q

By offloading deletion to Django-Q:

* Deletion is handled as a **background task**, so it doesn’t block the frontend.
* The database can process deletions more efficiently, especially when using **batch deletion**.
* Users get a smoother experience ,the code disappears instantly from their view, while the actual cleanup happens safely in the background.


### Deletion flow

<div align="center">

  <img src="django_auth_recovery_codes/docs/images/deletion_flowchart.png" alt="Code deletion flowchart" width="300">

</div>
---

### Batch deletion configuration

For projects with very large datasets, batch deletion can be enabled via the `DJANGO_AUTH_RECOVERY_CODES_BATCH_DELETE_SIZE` setting flag:

```python
# settings.py
DJANGO_AUTH_RECOVERY_CODES_BATCH_DELETE_SIZE = 1000
```

* If set, expired or soft-deleted codes will be removed in chunks of this size.
* If not set, all deletions happen in a single query.

This approach provides flexibility,  small apps can use one-shot deletes, while larger systems can safely handle deletions in manageable batches.

---


## Setting up Django-Q

The `django_auth_recovery_codes` library uses **Django-Q** internally. You don’t need to install it separately, but you must configure it in your Django project to ensure background tasks run properly.

---

### 1. Add Django-Q to Installed Apps

In your `settings.py`:

```python
INSTALLED_APPS = [
    ...
    'django_q',
]
```

---

### 2. Configure the Q\_CLUSTER

Example configuration:

```python
Q_CLUSTER = {
    'name': 'recovery_codes',
    'workers': 2,
    'timeout': 300,    # Maximum time (seconds) a task can run
    'retry': 600,      # Retry after 10 minutes if a task fails (retry must be greater than timeout)
    'recycle': 500,    # Recycle workers after this many tasks
    'compress': True,  # Compress data for storage
    'cpu_affinity': 1, # Assign workers to CPU cores
    'save_limit': 250, # Maximum number of task results to store
    'queue_limit': 500,# Maximum number of tasks in the queue
    'orm': 'default',  # Use the default database for task storage
}
```

For more configuration options, see the [official Django-Q documentation](https://django-q.readthedocs.io/en/latest/configure.html)

---

### 3. Running the Cluster

Don’t forget to start the Django-Q worker cluster so scheduled tasks actually run:

```bash
python manage.py qcluster
```

[⬆ Back to Top](#top)

---

## Django Auth Recovery flag settings 

These environment variables configure the **Django Auth Recovery** system, controlling email notifications, audit logs, recovery code display, rate limiting, cooldowns, and code management.

---
### **📌 Cheat Sheet: Variable Categories**

| Icon | Category                  | Jump to Section                                        |
| ---- | ------------------------- | ------------------------------------------------------ |
| 📧   | Email & Admin Settings    | [Email & Admin Settings Flags](#email--admin-settings-flags)       |
| 📝   | Audit & Logging           | [Audit & Logging Setting Flags](#audit--logging-setting-flags)     |
| 📄   | Code Display & Pagination | [Recovery Code Display Settings](#recovery-code-display-settings) |
| ⚡    | Rate Limiting & Caching   | [Rate Limiting & Caching](#rate-limiting--caching)                   |
| ⏱    | Cooldown Settings         | [Cooldown Settings Flags](#cooldown-settings-flags)                  |
| 🗂   | Code Management & Limits  | [Code Management & Limits](#code-management--limits)                 |

> Quick visual roadmap to jump to any section in the README.

---



## Email & Admin Settings Flags

These settings control which email and admin accounts are used for recovery code notifications and scheduled operations.

| Variable                                          | Description                                                                 |
| ------------------------------------------------- | --------------------------------------------------------------------------- |
| `DJANGO_AUTH_RECOVERY_CODES_ADMIN_SENDER_EMAIL`           | Admin email address used to receive generated reports after scheduled code deletions. |
| `DJANGO_AUTH_RECOVERY_CODE_ADMIN_EMAIL_HOST_USER` | The email address used by the application to send emails, typically the same as `EMAIL_HOST_USER` in your Django settings. |
| `DJANGO_AUTH_RECOVERY_CODE_ADMIN_USERNAME`        | Username associated with the admin account.                                  |

### Example Usage

Suppose the settings are:

- `DJANGO_AUTH_RECOVERY_CODES_ADMIN_SENDER_EMAIL = admin@example.com`  
- `DJANGO_AUTH_RECOVERY_CODE_ADMIN_EMAIL_HOST_USER = noreply@example.com`  
- `DJANGO_AUTH_RECOVERY_CODE_ADMIN_USERNAME = admin`  

Then the system will:

1. Send generated reports of scheduled recovery code deletions to `admin@example.com`.  
2. Use `noreply@example.com` as the sender for all automated recovery code emails.  
3. Associate actions with the admin username for auditing purposes.


---

## Audit & Logging Setting Flags

These settings control the auditing, logging, and retention of recovery code activity to ensure traceability and compliance.

| Variable                                                      | Description                                 |
| ------------------------------------------------------------- | ------------------------------------------- |
| `DJANGO_AUTH_RECOVERY_CODE_AUDIT_ENABLE_AUTO_CLEANUP`         | Enable automatic cleanup of audit logs.     |
| `DJANGO_AUTH_RECOVERY_CODE_AUDIT_RETENTION_DAYS`              | Number of days to retain audit logs.        |
| `DJANGO_AUTH_RECOVERY_CODE_PURGE_DELETE_SCHEDULER_USE_LOGGER` | Log scheduler operations during code purge. |
| `DJANGO_AUTH_RECOVERY_CODE_STORE_EMAIL_LOG`                   | Record activity of sent recovery emails.    |

### Example Usage

Suppose the settings are:

- `DJANGO_AUTH_RECOVERY_CODE_AUDIT_ENABLE_AUTO_CLEANUP = True`  
- `DJANGO_AUTH_RECOVERY_CODE_AUDIT_RETENTION_DAY