aboutsummaryrefslogtreecommitdiff
path: root/jb/models/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'jb/models/auth.py')
-rw-r--r--jb/models/auth.py65
1 files changed, 65 insertions, 0 deletions
diff --git a/jb/models/auth.py b/jb/models/auth.py
new file mode 100644
index 0000000..5f31bd3
--- /dev/null
+++ b/jb/models/auth.py
@@ -0,0 +1,65 @@
+import hashlib
+import hmac
+
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ EmailStr,
+ Field,
+ TypeAdapter,
+ computed_field,
+)
+
+from jb.config import settings
+
+
+def email_to_product_user_id(email: str) -> str:
+ """Return a deterministic, non-reversible product user ID for an email.
+
+ The same normalized email and secret salt always produce the same ID. Keep
+ the salt private and stable; changing it changes every generated ID.
+ """
+ salt_bytes = settings.magic_token_salt.get_secret_value().encode("utf-8")
+ if len(salt_bytes) < 32:
+ raise ValueError("salt must be at least 32 bytes")
+
+ if not email.isascii():
+ raise ValueError("email must contain ASCII characters only")
+
+ normalized_email = str(TypeAdapter(EmailStr).validate_python(email)).lower()
+ return hmac.new(
+ key=salt_bytes,
+ msg=normalized_email.encode("utf-8"),
+ digestmod=hashlib.sha256,
+ ).hexdigest()
+
+
+class AuthenticatedUser(BaseModel):
+ """A user that has been authenticated and exists in THL"""
+
+ email: EmailStr = Field()
+
+ @computed_field
+ def product_user_id(self) -> str:
+ return email_to_product_user_id(self.email)
+
+
+class AccountCreate(BaseModel):
+ email: EmailStr = Field()
+
+
+class AccountLogin(BaseModel):
+ email: EmailStr = Field()
+
+
+
+class MagicLinkExchangeRequest(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ token: str = Field(min_length=1)
+
+
+class SessionResponse(BaseModel):
+ session_token: str
+ token_type: str = "bearer"
+ expires_in: int