Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ repos:
files: ^efile_app/.*\.html$
args:
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
files=()
for file in "$@"; do
files+=("${file#efile_app/}")
files+=("$ROOT_DIR/$file")
done
(cd efile_app && uv run djlint --reformat "${files[@]}")
uv run --directory "$ROOT_DIR/efile_app" djlint --reformat "${files[@]}"
- --

- id: djlint-lint
Expand All @@ -32,11 +33,12 @@ repos:
files: ^efile_app/.*\.html$
args:
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
files=()
for file in "$@"; do
files+=("${file#efile_app/}")
files+=("$ROOT_DIR/$file")
done
(cd efile_app && uv run djlint "${files[@]}")
uv run --directory "$ROOT_DIR/efile_app" djlint "${files[@]}"
- --

- id: css-beautify
Expand All @@ -46,11 +48,12 @@ repos:
files: ^efile_app/.*\.css$
args:
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
files=()
for file in "$@"; do
files+=("${file#efile_app/}")
files+=("$ROOT_DIR/$file")
done
(cd efile_app && uv run css-beautify -r "${files[@]}")
uv run --directory "$ROOT_DIR/efile_app" css-beautify -r "${files[@]}"
- --

- id: js-beautify
Expand All @@ -60,32 +63,38 @@ repos:
files: ^efile_app/.*\.js$
args:
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
files=()
for file in "$@"; do
files+=("${file#efile_app/}")
files+=("$ROOT_DIR/$file")
done
(cd efile_app && uv run js-beautify -r "${files[@]}")
uv run --directory "$ROOT_DIR/efile_app" js-beautify -r "${files[@]}"
- --

- id: ty
name: ty (type check)
entry: bash -lc
entry: bash -c
language: system
types: [python]
pass_filenames: false
args:
- cd efile_app && uv run ty check
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
uv run --directory "$ROOT_DIR/efile_app" ty check

# Run tests on pre-push to keep commits snappy
- repo: local
hooks:
- id: pytest
name: pytest (pre-push)
entry: bash -lc
entry: bash -c
language: system
stages: [pre-push]
pass_filenames: false
args:
- cd efile_app && uv run pytest -q
- |
ROOT_DIR=$(git rev-parse --show-toplevel)
uv run --directory "$ROOT_DIR/efile_app" pytest -q

# If needed, override Django settings here instead of pyproject:
# - --ds=efile.settings
14 changes: 12 additions & 2 deletions efile_app/efile/api/auth_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,19 @@ def user_login(request):
user = authenticate(request, username=username, password=password, jurisdiction=jurisdiction)

if user is not None:
auth_tokens = request.session.get("auth_tokens", {})
request.session.flush()
login(request, user)
request.session["auth_tokens"] = auth_tokens
request.session["user_email"] = user.email
request.session["jurisdiction"] = jurisdiction
return AuthAPIViews.success_response(
{"user_id": user.id, "username": user.username, "email": user.email, "is_authenticated": True},
{
"user_id": user.id,
"username": user.account_email,
"email": user.email,
"is_authenticated": True,
},
"Login successful",
)
else:
Expand Down Expand Up @@ -132,7 +142,7 @@ def user_profile(request):
"external_firm_data": external_data,
# Local user data (if authenticated)
"id": request.user.id if request.user.is_authenticated else None,
"username": request.user.username if request.user.is_authenticated else "guest",
"username": request.user.account_email if request.user.is_authenticated else "guest",
"email": request.user.email if request.user.is_authenticated else request.session.get("user_email"),
"first_name": self_json["firstName"],
"last_name": self_json["lastName"],
Expand Down
15 changes: 2 additions & 13 deletions efile_app/efile/api/suffolk_api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from requests.exceptions import RequestException

from efile.utils.case_data_utils import get_case_data, update_case_data
from efile.utils.jurisdiction_stuff import get_jurisdiction_from_request
from efile.utils.jurisdiction_stuff import get_jurisdiction_from_request, get_jurisdiction_token
from efile.utils.proxy_connection import get_headers

logger = logging.getLogger(__name__)
Expand All @@ -24,18 +24,7 @@ def get_tyler_token(request, jurisdiction=None):
if jurisdiction is None:
jurisdiction = get_jurisdiction_from_request(request)

# Fallback to session
auth_tokens = request.session.get("auth_tokens", {})
logger.debug(f"Auth tokens in session: {auth_tokens}")

# Try different Tyler token key formats
tyler_token = (
auth_tokens.get(f"TYLER-TOKEN-{jurisdiction.upper()}")
or auth_tokens.get(f"tyler_token_{jurisdiction}")
or auth_tokens.get(f"tyler-token-{jurisdiction}")
)

return tyler_token
return get_jurisdiction_token(request, jurisdiction)


def set_access_control_headers(response):
Expand Down
26 changes: 18 additions & 8 deletions efile_app/efile/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend

from efile.utils.account_ids import jurisdiction_account_username
from efile.utils.jurisdiction_stuff import get_jurisdiction_from_request
from efile.utils.proxy_connection import auth_with_tyler_api

Expand Down Expand Up @@ -47,28 +48,37 @@ def get_user(self, user_id):
return None

def _get_or_create_user(self, username, auth_data, jurisdiction):
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = None
user = User.objects.filter(
tyler_username__iexact=username,
tyler_jurisdiction__iexact=jurisdiction,
).first()
if user is None:
# Accounts created before ``tyler_username`` was introduced stored the
# Tyler login directly in Django's username field.
user = User.objects.filter(
username__iexact=username,
tyler_jurisdiction__iexact=jurisdiction,
tyler_username="",
).first()

user_data = self._extract_user_data(auth_data, username, jurisdiction)
if not user:
user = User.objects.create_user(
username=username,
username=jurisdiction_account_username(username, jurisdiction),
tyler_jurisdiction=jurisdiction,
tyler_username=username,
tyler_user_id=user_data.get("user_id", None),
email=user_data.get("email", username),
first_name=user_data.get("first_name", ""),
last_name=user_data.get("last_name", ""),
)
else:
user.tyler_jurisdiction = jurisdiction
user.tyler_username = username
user.tyler_user_id = user_data.get("user_id", None)
user.email = user_data.get("email", username)
user.save()
user.save(update_fields=["tyler_username", "tyler_user_id", "email", "updated_at"])

logger.info("Created new user: %s, %s", user.username, user.email)
logger.info("Authenticated local account %s for %s", user.pk, jurisdiction)
return user

def _extract_user_data(self, auth_data, username, jurisdiction):
Expand Down
20 changes: 18 additions & 2 deletions efile_app/efile/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django import forms
from django.contrib.auth import get_user_model
from django.db.models import Q

from efile.utils.account_ids import jurisdiction_account_username

User = get_user_model()

Expand All @@ -22,6 +25,10 @@ class EFilePasswordResetForm(forms.Form):


class EFileRegistrationForm(forms.Form):
def __init__(self, *args, jurisdiction=None, **kwargs):
super().__init__(*args, **kwargs)
self.jurisdiction = jurisdiction

# Legal Name
first_name = forms.CharField(
max_length=100,
Expand Down Expand Up @@ -135,7 +142,12 @@ class EFileRegistrationForm(forms.Form):

def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).exists():
existing_accounts = User.objects.filter(
Q(tyler_username__iexact=email) | Q(username__iexact=email) | Q(email__iexact=email)
)
if self.jurisdiction:
existing_accounts = existing_accounts.filter(tyler_jurisdiction__iexact=self.jurisdiction)
if existing_accounts.exists():
raise forms.ValidationError("A user with this email already exists.")
return email

Expand All @@ -160,9 +172,13 @@ def clean(self):
return cleaned_data

def save(self):
if not self.jurisdiction:
raise ValueError("A jurisdiction is required to create an eFile account")
user = User.objects.create_user(
username=self.cleaned_data["email"],
username=jurisdiction_account_username(self.cleaned_data["email"], self.jurisdiction),
email=self.cleaned_data["email"],
tyler_username=self.cleaned_data["email"],
tyler_jurisdiction=self.jurisdiction,
password=self.cleaned_data["password"],
first_name=self.cleaned_data["first_name"],
last_name=self.cleaned_data["last_name"],
Expand Down
26 changes: 26 additions & 0 deletions efile_app/efile/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
from django.contrib.auth import logout

from efile.utils.jurisdiction_stuff import get_jurisdiction_from_request


class JurisdictionSessionMiddleware:
"""End an authenticated session before it can cross a jurisdiction boundary."""

def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
jurisdiction = get_jurisdiction_from_request(request)
user = getattr(request, "user", None)
account_jurisdiction = getattr(user, "tyler_jurisdiction", "")
if (
jurisdiction
and getattr(user, "is_authenticated", False)
and account_jurisdiction
and account_jurisdiction.casefold() != jurisdiction.casefold()
):
logout(request)

return self.get_response(request)


class NoCacheHTMLMiddleware:
"""Stop browsers from reusing a stale rendered page without asking the server first.

Expand Down
31 changes: 31 additions & 0 deletions efile_app/efile/migrations/0003_jurisdiction_specific_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.db import migrations, models


def copy_existing_tyler_usernames(apps, schema_editor):
UserProfile = apps.get_model("efile", "UserProfile")
for user in UserProfile.objects.exclude(tyler_jurisdiction="").iterator():
user.tyler_username = user.username
user.save(update_fields=["tyler_username"])


class Migration(migrations.Migration):
dependencies = [
("efile", "0002_filing_drafts"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="tyler_username",
field=models.CharField(blank=True, max_length=254),
),
migrations.RunPython(copy_existing_tyler_usernames, migrations.RunPython.noop),
migrations.AddConstraint(
model_name="userprofile",
constraint=models.UniqueConstraint(
condition=~models.Q(tyler_username=""),
fields=("tyler_jurisdiction", "tyler_username"),
name="unique_tyler_account_per_jurisdiction",
),
),
]
17 changes: 16 additions & 1 deletion efile_app/efile/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ class UserProfile(AbstractUser):
Extended user profile to store eFile registration information.
"""

# TODO(brycew): what happens if someone is trying to do stuff in multiple jurisdictions?
# Tyler identities are jurisdiction-specific; ``tyler_username`` may repeat
# across jurisdictions while each row remains a separate local account.
tyler_jurisdiction = models.CharField(max_length=20)
tyler_user_id = models.CharField(max_length=100, blank=True, null=True)
tyler_username = models.CharField(max_length=254, blank=True)

# TODO(brycew): uncomment when https://github.com/SuffolkLITLab/EfileProxyServer/issues/334 is in
# token_expires_at = models.DateTimeField(blank=True, null=True)
Expand All @@ -30,6 +32,19 @@ class UserProfile(AbstractUser):
class Meta:
verbose_name = "User Profile"
verbose_name_plural = "User Profiles"
constraints = [
models.UniqueConstraint(
fields=["tyler_jurisdiction", "tyler_username"],
condition=~models.Q(tyler_username=""),
name="unique_tyler_account_per_jurisdiction",
)
]

@property
def account_email(self):
"""The external Tyler login shown to the user, never the internal username."""

return self.tyler_username or self.email or self.username


class FilingDraft(models.Model):
Expand Down
1 change: 1 addition & 0 deletions efile_app/efile/settings_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"efile.middleware.JurisdictionSessionMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"efile.middleware.NoCacheHTMLMiddleware",
Expand Down
Loading