Plain is headed towards 1.0! Subscribe for development updates →

 1from __future__ import annotations
 2
 3from typing import Any
 4
 5from plain import models
 6
 7from . import validators
 8from .hashers import (
 9    hash_password,
10    identify_hasher,
11)
12
13
14class PasswordField(models.CharField):
15    def __init__(self, *args: Any, **kwargs: Any) -> None:
16        kwargs["max_length"] = 128
17        kwargs.setdefault(
18            "validators",
19            [
20                validators.MinimumLengthValidator(),
21                validators.CommonPasswordValidator(),
22                validators.NumericPasswordValidator(),
23            ],
24        )
25        super().__init__(*args, **kwargs)
26
27    def deconstruct(self) -> tuple[str, str, tuple[Any, ...], dict[str, Any]]:
28        name, path, args, kwargs = super().deconstruct()
29        if kwargs.get("max_length") == 128:
30            del kwargs["max_length"]
31        return name, path, args, kwargs
32
33    def pre_save(self, model_instance: models.Model, add: bool) -> str:
34        value = super().pre_save(model_instance, add)
35
36        if value and not self._is_hashed(value):
37            value = hash_password(value)
38            # Set the hashed value back on the instance immediately too
39            setattr(model_instance, self.attname, value)
40
41        return value
42
43    @staticmethod
44    def _is_hashed(value: str) -> bool:
45        try:
46            identify_hasher(value)
47            return True
48        except ValueError:
49            return False