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