Allauth extracted to its own repository (~/dev/mizan-allauth)
The auth-provider concern becomes a dedicated Django system. Removed from mizan: the Django integration (mizan/integrations/allauth — auth contexts + ~15 form wrappers), the legacy/ pre-kernel TypeScript client, the allauth and webauthn dependency extras (fido2 was consumed only by the WebAuthn form wrappers), and the HEADLESS_JWT_* settings fallbacks — the allauth-headless compat seam belongs to the dedicated system, not to mizan's JWT module. Duplicate-name registration in discovery now surfaces a warning instead of passing silently. README claims updated to point at mizan-allauth; the root README's hand-maintained status matrix collapsed into the tests/afi conformance suite as the parity authority. OWED_SURFACE.md refreshed against the post-extraction tree (22 units). mizan-django suite: 350 passed, 21 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,6 @@ function. Typed React client generated. Invalidation automatic.
|
||||
|
||||
```bash
|
||||
uv add "mizan[channels]"
|
||||
# or with allauth integration:
|
||||
uv add "mizan[channels,allauth]"
|
||||
```
|
||||
|
||||
## Setup
|
||||
@@ -116,6 +114,9 @@ class ContactForm(mizanFormMixin, forms.Form):
|
||||
Auto-registers `contact.schema`, `contact.validate`, `contact.submit`. Frontend
|
||||
gets `useContactForm()`.
|
||||
|
||||
Auth-provider forms (django-allauth login, signup, MFA, WebAuthn) live in the
|
||||
dedicated `mizan-allauth` repository, built on this mixin.
|
||||
|
||||
## Channels
|
||||
|
||||
WebSocket-native RPC via a flag flip:
|
||||
|
||||
@@ -25,12 +25,6 @@ channels = [
|
||||
"channels>=4.0",
|
||||
"channels-redis>=4.0",
|
||||
]
|
||||
allauth = [
|
||||
"django-allauth>=65.0",
|
||||
]
|
||||
webauthn = [
|
||||
"fido2>=2.0",
|
||||
]
|
||||
shapes = [
|
||||
"django-readers>=2.0",
|
||||
]
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""
|
||||
mizan Allauth Integration
|
||||
|
||||
Backend support for django-allauth with mizan server functions.
|
||||
|
||||
Provides:
|
||||
- Auth contexts (auth_status, user) - required by frontend allauth module
|
||||
- Allauth form wrappers - expose allauth forms as server functions
|
||||
|
||||
Usage:
|
||||
# In your app's apps.py
|
||||
class MyAppConfig(AppConfig):
|
||||
def ready(self):
|
||||
import mizan.allauth.forms # noqa - registers forms
|
||||
import mizan.allauth.contexts # noqa - registers contexts
|
||||
"""
|
||||
|
||||
from .contexts import auth_status, user, AuthStatusOutput, UserOutput
|
||||
|
||||
__all__ = [
|
||||
"auth_status",
|
||||
"user",
|
||||
"AuthStatusOutput",
|
||||
"UserOutput",
|
||||
]
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
Auth contexts for mizan Allauth integration.
|
||||
|
||||
These are the core auth primitives that the frontend allauth module depends on.
|
||||
Separated into two concerns:
|
||||
|
||||
- auth_status: Authentication state and permission guards (fast, no DB hit with JWT)
|
||||
- user: Full user profile data (may require DB query for JWT auth)
|
||||
|
||||
Both are registered as global contexts for SSR hydration.
|
||||
"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mizan.client import client
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Auth Status Context
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AuthStatusOutput(BaseModel):
|
||||
"""Authentication status and permission guards."""
|
||||
|
||||
is_authenticated: bool
|
||||
user_id: int | None = None
|
||||
is_staff: bool = False
|
||||
is_superuser: bool = False
|
||||
|
||||
|
||||
@client(context="global")
|
||||
def auth_status(request: HttpRequest) -> AuthStatusOutput:
|
||||
"""
|
||||
Auth status context - provides authentication state and guards.
|
||||
|
||||
This works identically for both session and JWT auth. The data comes
|
||||
from the request.user object (either full User or JWTUser with claims).
|
||||
|
||||
Frontend:
|
||||
const auth = useAuthStatus()
|
||||
if (auth.is_authenticated) { ... }
|
||||
if (auth.is_staff) { ... }
|
||||
"""
|
||||
user = request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
return AuthStatusOutput(is_authenticated=False)
|
||||
|
||||
return AuthStatusOutput(
|
||||
is_authenticated=True,
|
||||
user_id=user.id,
|
||||
is_staff=user.is_staff,
|
||||
is_superuser=user.is_superuser,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# User Profile Context
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class UserOutput(BaseModel):
|
||||
"""Full user profile data."""
|
||||
|
||||
id: int
|
||||
email: str
|
||||
first_name: str = ""
|
||||
last_name: str = ""
|
||||
|
||||
|
||||
@client(context="global")
|
||||
def user(request: HttpRequest) -> UserOutput | None:
|
||||
"""
|
||||
User profile context - provides full user data.
|
||||
|
||||
Unlike auth_status, this may require a DB query (for JWT auth where
|
||||
the user object is a minimal JWTUser with only claims).
|
||||
|
||||
Returns None if not authenticated.
|
||||
|
||||
Frontend:
|
||||
const user = useUser()
|
||||
if (user) {
|
||||
console.log(user.email)
|
||||
}
|
||||
"""
|
||||
req_user = request.user
|
||||
|
||||
if not req_user.is_authenticated:
|
||||
return None
|
||||
|
||||
# Check if we have full user data or just JWT claims
|
||||
if hasattr(req_user, "email") and req_user.email:
|
||||
# Full User object (session auth)
|
||||
return UserOutput(
|
||||
id=req_user.id,
|
||||
email=req_user.email,
|
||||
first_name=getattr(req_user, "first_name", "") or "",
|
||||
last_name=getattr(req_user, "last_name", "") or "",
|
||||
)
|
||||
|
||||
# JWTUser - need to fetch from DB
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
try:
|
||||
db_user = User.objects.get(pk=req_user.id)
|
||||
return UserOutput(
|
||||
id=db_user.id,
|
||||
email=db_user.email,
|
||||
first_name=db_user.first_name or "",
|
||||
last_name=db_user.last_name or "",
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
@@ -1,408 +0,0 @@
|
||||
"""
|
||||
Allauth forms as mizan server functions.
|
||||
|
||||
This module wraps allauth forms with mizanFormMixin, exposing them as
|
||||
typed server functions for the React frontend.
|
||||
|
||||
Each form becomes three server functions:
|
||||
- {name}.schema - Get form field definitions
|
||||
- {name}.validate - Validate form data
|
||||
- {name}.submit - Submit form
|
||||
|
||||
Import this module in your app's ready() to register the forms:
|
||||
|
||||
class MyAppConfig(AppConfig):
|
||||
def ready(self):
|
||||
import mizan.allauth.forms # noqa
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
from mizan.forms import mizanFormMixin, mizanFormMeta
|
||||
|
||||
# Account forms
|
||||
from allauth.account.forms import (
|
||||
AddEmailForm,
|
||||
ChangePasswordForm,
|
||||
ConfirmLoginCodeForm,
|
||||
LoginForm,
|
||||
RequestLoginCodeForm,
|
||||
ResetPasswordForm,
|
||||
ResetPasswordKeyForm,
|
||||
SetPasswordForm,
|
||||
SignupForm,
|
||||
UserTokenForm,
|
||||
)
|
||||
|
||||
# Password reauthentication form - conditionally import
|
||||
try:
|
||||
from allauth.account.forms import ReauthenticateForm
|
||||
|
||||
HAS_REAUTH = True
|
||||
except ImportError:
|
||||
HAS_REAUTH = False
|
||||
|
||||
# MFA forms - conditionally import
|
||||
try:
|
||||
from allauth.mfa.base.forms import AuthenticateForm as MFAAuthenticateForm
|
||||
from allauth.mfa.base.forms import ReauthenticateForm as MFAReauthenticateForm
|
||||
from allauth.mfa.totp.forms import ActivateTOTPForm, DeactivateTOTPForm
|
||||
from allauth.mfa.recovery_codes.forms import GenerateRecoveryCodesForm
|
||||
|
||||
HAS_MFA = True
|
||||
except ImportError:
|
||||
HAS_MFA = False
|
||||
|
||||
# WebAuthn forms (if available)
|
||||
try:
|
||||
from allauth.mfa.webauthn.forms import AuthenticateWebAuthnForm
|
||||
|
||||
HAS_WEBAUTHN = True
|
||||
except ImportError:
|
||||
HAS_WEBAUTHN = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mizan.forms.schemas import FormValidation
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Account Forms
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class mizanLoginForm(LoginForm, mizanFormMixin):
|
||||
"""Sign in with email and password."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="login",
|
||||
title="Sign In",
|
||||
subtitle="Welcome back. Enter your credentials to continue.",
|
||||
submit_label="Sign In",
|
||||
live_validation=False, # Don't validate credentials as user types
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.login(request)
|
||||
return None
|
||||
|
||||
|
||||
class mizanSignupForm(SignupForm, mizanFormMixin):
|
||||
"""Create a new account."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="signup",
|
||||
title="Create Account",
|
||||
subtitle="Enter your details to get started.",
|
||||
submit_label="Create Account",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save(request)
|
||||
return None
|
||||
|
||||
|
||||
class mizanAddEmailForm(AddEmailForm, mizanFormMixin):
|
||||
"""Add another email address to your account."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="add_email",
|
||||
title="Add Email Address",
|
||||
subtitle="Add another email address to your account.",
|
||||
submit_label="Add Email",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanChangePasswordForm(ChangePasswordForm, mizanFormMixin):
|
||||
"""Change your account password."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="change_password",
|
||||
title="Change Password",
|
||||
subtitle="Update your password to keep your account secure.",
|
||||
submit_label="Change Password",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanSetPasswordForm(SetPasswordForm, mizanFormMixin):
|
||||
"""Set a password for accounts created via social login."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="set_password",
|
||||
title="Set Password",
|
||||
subtitle="Create a password for your account.",
|
||||
submit_label="Set Password",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanResetPasswordForm(ResetPasswordForm, mizanFormMixin):
|
||||
"""Request a password reset email."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="reset_password",
|
||||
title="Reset Password",
|
||||
subtitle="Enter your email address and we'll send you a link to reset your password.",
|
||||
submit_label="Send Reset Link",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save(request)
|
||||
return None
|
||||
|
||||
|
||||
class mizanResetPasswordKeyForm(ResetPasswordKeyForm, mizanFormMixin):
|
||||
"""Set a new password using a reset key."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="reset_password_from_key",
|
||||
title="Set New Password",
|
||||
subtitle="Enter your new password below.",
|
||||
submit_label="Reset Password",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanRequestLoginCodeForm(RequestLoginCodeForm, mizanFormMixin):
|
||||
"""Request a login code via email."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="request_login_code",
|
||||
title="Sign In with Code",
|
||||
subtitle="Enter your email address and we'll send you a login code.",
|
||||
submit_label="Send Code",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanConfirmLoginCodeForm(ConfirmLoginCodeForm, mizanFormMixin):
|
||||
"""Confirm a login code."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="confirm_login_code",
|
||||
title="Enter Code",
|
||||
subtitle="Enter the code we sent to your email.",
|
||||
submit_label="Verify Code",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
class mizanUserTokenForm(UserTokenForm, mizanFormMixin):
|
||||
"""Verify an email with a token."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="user_token",
|
||||
title="Verify Email",
|
||||
subtitle="Enter the verification code from your email.",
|
||||
submit_label="Verify",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
# Password reauthentication - conditionally define
|
||||
if HAS_REAUTH:
|
||||
|
||||
class mizanReauthenticateForm(ReauthenticateForm, mizanFormMixin):
|
||||
"""Re-authenticate with password for sensitive actions."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="reauthenticate",
|
||||
title="Confirm Your Identity",
|
||||
subtitle="Please enter your password to continue.",
|
||||
submit_label="Confirm",
|
||||
live_validation=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
from allauth.account.internal.flows import reauthentication
|
||||
|
||||
reauthentication.reauthenticate_by_password(request)
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MFA Forms
|
||||
# =============================================================================
|
||||
|
||||
if HAS_MFA:
|
||||
|
||||
class mizanMFAAuthenticateForm(MFAAuthenticateForm, mizanFormMixin):
|
||||
"""Authenticate with MFA during login."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="mfa_authenticate",
|
||||
title="Two-Factor Authentication",
|
||||
subtitle="Enter your authentication code to continue.",
|
||||
submit_label="Verify",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
class mizanMFAReauthenticateForm(MFAReauthenticateForm, mizanFormMixin):
|
||||
"""Re-authenticate with MFA for sensitive actions."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="mfa_reauthenticate",
|
||||
title="Confirm Your Identity",
|
||||
subtitle="Enter your authentication code to continue.",
|
||||
submit_label="Confirm",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
class mizanActivateTOTPForm(ActivateTOTPForm, mizanFormMixin):
|
||||
"""Activate TOTP authenticator."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="activate_totp",
|
||||
title="Set Up Authenticator",
|
||||
subtitle="Enter the code from your authenticator app to complete setup.",
|
||||
submit_label="Activate",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
class mizanDeactivateTOTPForm(DeactivateTOTPForm, mizanFormMixin):
|
||||
"""Deactivate TOTP authenticator."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="deactivate_totp",
|
||||
title="Disable Authenticator",
|
||||
subtitle="Enter your password to disable two-factor authentication.",
|
||||
submit_label="Disable",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
class mizanGenerateRecoveryCodesForm(GenerateRecoveryCodesForm, mizanFormMixin):
|
||||
"""Generate new recovery codes."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="generate_recovery_codes",
|
||||
title="Recovery Codes",
|
||||
subtitle="Generate new recovery codes for your account.",
|
||||
submit_label="Generate Codes",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
|
||||
|
||||
if HAS_WEBAUTHN:
|
||||
|
||||
class mizanAuthenticateWebAuthnForm(AuthenticateWebAuthnForm, mizanFormMixin):
|
||||
"""Authenticate with WebAuthn security key."""
|
||||
|
||||
mizan = mizanFormMeta(
|
||||
name="webauthn_authenticate",
|
||||
title="Security Key",
|
||||
subtitle="Use your security key to authenticate.",
|
||||
submit_label="Use Security Key",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs(cls, request: HttpRequest) -> dict[str, Any]:
|
||||
return {"request": request, "user": request.user}
|
||||
|
||||
def on_submit_success(self, request: HttpRequest) -> dict | None:
|
||||
self.save()
|
||||
return None
|
||||
@@ -1,10 +1,3 @@
|
||||
"""
|
||||
JWT Hybrid Settings
|
||||
|
||||
Configuration is read from Django settings with sensible defaults.
|
||||
Supports both symmetric (HS256) and asymmetric (RS256) algorithms.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
@@ -13,8 +6,6 @@ from django.conf import settings as django_settings
|
||||
|
||||
@dataclass
|
||||
class JWTSettings:
|
||||
"""JWT configuration."""
|
||||
|
||||
# Signing keys
|
||||
private_key: str # Used for signing (required)
|
||||
public_key: str # Used for verification (same as private for HS256)
|
||||
@@ -33,26 +24,8 @@ class JWTSettings:
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> JWTSettings:
|
||||
"""
|
||||
Load JWT settings from Django settings.
|
||||
|
||||
Settings:
|
||||
JWT_PRIVATE_KEY: Signing key (required)
|
||||
JWT_PUBLIC_KEY: Verification key (defaults to private key for HS256)
|
||||
JWT_ALGORITHM: Algorithm to use (default: HS256)
|
||||
JWT_ACCESS_TOKEN_EXPIRES_IN: Access token lifetime (default: 300)
|
||||
JWT_REFRESH_TOKEN_EXPIRES_IN: Refresh token lifetime (default: 604800)
|
||||
JWT_VALIDATE_SESSION: Validate session on token use (default: True)
|
||||
JWT_ROTATE_REFRESH_TOKEN: Rotate refresh tokens (default: True)
|
||||
"""
|
||||
private_key = getattr(django_settings, "JWT_PRIVATE_KEY", None)
|
||||
|
||||
if not private_key:
|
||||
# Fall back to allauth setting if available (for compatibility)
|
||||
headless_key = getattr(django_settings, "HEADLESS_JWT_PRIVATE_KEY", None)
|
||||
if headless_key:
|
||||
private_key = headless_key
|
||||
|
||||
if private_key is None:
|
||||
raise ValueError(
|
||||
"JWT_PRIVATE_KEY must be set in Django settings. "
|
||||
@@ -60,7 +33,6 @@ def get_settings() -> JWTSettings:
|
||||
"For RS256, use a PEM-encoded RSA private key."
|
||||
)
|
||||
|
||||
# Auto-detect algorithm based on key format if not explicitly set
|
||||
algorithm = getattr(django_settings, "JWT_ALGORITHM", None)
|
||||
|
||||
if algorithm is None:
|
||||
@@ -100,14 +72,10 @@ def get_settings() -> JWTSettings:
|
||||
public_key=public_key,
|
||||
algorithm=algorithm,
|
||||
access_token_expires_in=getattr(
|
||||
django_settings,
|
||||
"JWT_ACCESS_TOKEN_EXPIRES_IN",
|
||||
getattr(django_settings, "HEADLESS_JWT_ACCESS_TOKEN_EXPIRES_IN", 300),
|
||||
django_settings, "JWT_ACCESS_TOKEN_EXPIRES_IN", 300
|
||||
),
|
||||
refresh_token_expires_in=getattr(
|
||||
django_settings,
|
||||
"JWT_REFRESH_TOKEN_EXPIRES_IN",
|
||||
getattr(django_settings, "HEADLESS_JWT_REFRESH_TOKEN_EXPIRES_IN", 604800),
|
||||
django_settings, "JWT_REFRESH_TOKEN_EXPIRES_IN", 604800
|
||||
),
|
||||
validate_session=getattr(
|
||||
django_settings, "JWT_VALIDATE_SESSION", True
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
"""
|
||||
mizan Auto-Discovery
|
||||
|
||||
Scans Django apps for server functions following the 'clients' layer convention:
|
||||
- <app>/clients.py
|
||||
- <app>/clients/**/*.py
|
||||
|
||||
Usage in urls.py:
|
||||
from mizan.setup.discovery import mizan_clients
|
||||
|
||||
mizan_clients('apps') # Scans apps/*/clients.py
|
||||
mizan_clients('mizan', 'allauth') # Scans mizan/allauth/**/*.py
|
||||
|
||||
This replaces manual "import to register" patterns with explicit auto-discovery.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from mizan._vendor.app_visitor import DjangoAppVisitor, get_members
|
||||
@@ -21,14 +6,13 @@ from mizan._vendor.app_visitor import DjangoAppVisitor, get_members
|
||||
from mizan_core.registry import register, get_function
|
||||
from mizan_core.client.function import ServerFunction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _RegisterServerFunctions:
|
||||
"""Visitor handler that registers ServerFunction subclasses."""
|
||||
|
||||
def on_module(
|
||||
self, app_name: str, path_parts: list[str], members: list[tuple[str, Any]]
|
||||
) -> None:
|
||||
"""Process discovered module members."""
|
||||
for name, member in members:
|
||||
# Register ServerFunction subclasses
|
||||
if (
|
||||
@@ -47,44 +31,23 @@ class _RegisterServerFunctions:
|
||||
try:
|
||||
register(member, fn_name)
|
||||
except ValueError:
|
||||
# Already registered with different class - skip
|
||||
pass
|
||||
logger.warning(
|
||||
"Server function name %r already registered with a "
|
||||
"different class; skipping %s.%s",
|
||||
fn_name,
|
||||
member.__module__,
|
||||
member.__qualname__,
|
||||
)
|
||||
|
||||
|
||||
# Scans <app>/<layer>.py and <app>/<layer>/**/*.py under apps_root
|
||||
def mizan_clients(apps_root: str, layer: str = "clients") -> None:
|
||||
"""
|
||||
Discover and register server functions from Django apps.
|
||||
|
||||
Scans for the specified layer (default: 'clients') in each app:
|
||||
- <app>/<layer>.py
|
||||
- <app>/<layer>/**/*.py
|
||||
|
||||
Args:
|
||||
apps_root: Root package containing Django apps (e.g., 'apps')
|
||||
layer: Module name pattern to scan (default: 'clients')
|
||||
|
||||
Example:
|
||||
# In urls.py
|
||||
mizan_clients('apps') # Scans apps/*/clients.py
|
||||
mizan_clients('apps', 'functions') # Scans apps/*/functions.py
|
||||
"""
|
||||
visitor = DjangoAppVisitor(layer=layer, apps_root=apps_root)
|
||||
visitor.visit(_RegisterServerFunctions())
|
||||
|
||||
|
||||
# Registers server functions from one module path, e.g. 'mizan.jwt.functions'
|
||||
def mizan_module(module_path: str) -> None:
|
||||
"""
|
||||
Register server functions from a specific module.
|
||||
|
||||
Use this for library modules that don't follow the app convention.
|
||||
|
||||
Args:
|
||||
module_path: Full module path (e.g., 'mizan.integrations.allauth')
|
||||
|
||||
Example:
|
||||
mizan_module('mizan.integrations.allauth')
|
||||
mizan_module('mizan.jwt.functions')
|
||||
"""
|
||||
members = get_members(module_path)
|
||||
handler = _RegisterServerFunctions()
|
||||
handler.on_module("", [], members)
|
||||
|
||||
Reference in New Issue
Block a user