Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
"filename": "config/settings/base.py",
"hashed_secret": "147864c9194800dfcf8618ba6d2e989afad8c9ef",
"is_verified": false,
"line_number": 447
"line_number": 450
}
],
"config/settings/ci.py": [
Expand Down Expand Up @@ -287,5 +287,5 @@
}
]
},
"generated_at": "2026-01-07T07:05:40Z"
"generated_at": "2026-01-12T09:26:50Z"
}
5 changes: 5 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
AUTH_USER_MODEL = "users.User"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url
LOGIN_REDIRECT_URL = "home"
AMENAGEMENT_LOGIN_REDIRECT_URL = "home"
HAIE_LOGIN_REDIRECT_URL = "petition_project_list"
# https://docs.djangoproject.com/en/dev/ref/settings/#login-url
LOGIN_URL = "login"

Expand Down Expand Up @@ -148,6 +150,7 @@
"envergo.analytics.middleware.SetVisitorIdCookie",
"envergo.middleware.rate_limiting.RateLimitingMiddleware",
"envergo.analytics.middleware.HandleMtmValues",
"envergo.petitions.middleware.HandleInvitationTokenMiddleware",
]

# STATIC
Expand Down Expand Up @@ -484,3 +487,5 @@
SECURE_CSP_REPORT_ONLY = {}

RATELIMIT_RATE = "100/m"

INVITATION_TOKEN_COOKIE_NAME = "invitation_token"
109 changes: 109 additions & 0 deletions envergo/petitions/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from datetime import timedelta

from django.conf import settings
from django.contrib import messages
from django.urls import reverse

from envergo.petitions.models import InvitationToken


class HandleInvitationTokenMiddleware:
"""Handle invitation tokens.

Store invitations token in session if the user is not logged in.
Process the invitation if it is valid.
"""

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

def __call__(self, request):

url_token = request.GET.get(settings.INVITATION_TOKEN_COOKIE_NAME)
cookie_token = request.COOKIES.get(settings.INVITATION_TOKEN_COOKIE_NAME)
delete_cookie_token = False

# User is authenticated. We look for invitation tokens in url
# or in session data.
if request.user.is_authenticated:
if url_token:
self.process_token(request, url_token)

if cookie_token:
self.process_token(request, cookie_token)
delete_cookie_token = True

# We process the invitation to prevent a 403 on the requested url
response = self.get_response(request)

# Clear the cookie token if it exists
if delete_cookie_token:
self.clear_token(response)

# User is not authenticated and an invitation token is found in
# the url. We just store the token in session to be used later.
if url_token and not request.user.is_authenticated:
self.store_token(request, response, url_token)

return response

def store_token(self, request, response, token):
"""Store the given token in session.

Note : le cookie respecte les contraintes imposées par la CNIL
peut donc être exempté de consentement.

> En ce qui concerne les traceurs non soumis au consentement,
on peut évoquer […] les traceurs destinés à l’authentification auprès d’un
service…

https://www.cnil.fr/fr/cookies-et-autres-traceurs/que-dit-la-loi

"""

lifetime = timedelta(30 * 13) # 13 months
response.set_cookie(
settings.INVITATION_TOKEN_COOKIE_NAME,
token,
max_age=lifetime.total_seconds(),
domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=True,
samesite=settings.SESSION_COOKIE_SAMESITE,
)

register_url = reverse("register")

messages.info(
request,
f"""
Pour accéder au dossier en tant qu’invité,
connectez-vous ou <a href="{register_url}">créez un compte</a>
sur le portail du Guichet Unique de la Haie.
""",
)

def process_token(self, request, token):
"""Accepts the invitation."""

invitation = InvitationToken.objects.filter(token=token).first()

if invitation:
if invitation.is_valid(request.user):
invitation.user = request.user
invitation.save()

messages.info(request, "Un dossier a été rattaché à votre compte.")
else:
messages.warning(
request,
"""
Le lien d'invitation utilisé n'est plus valide.
Il a peut-être déjà été utilisé ou a expiré.
Veuillez contacter la personne qui vous l'a transmis pour obtenir
un nouveau lien.""",
)

def clear_token(self, response):
response.delete_cookie(settings.INVITATION_TOKEN_COOKIE_NAME)
11 changes: 9 additions & 2 deletions envergo/petitions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,16 @@ class Meta:
verbose_name = "Jeton d'invitation"
verbose_name_plural = "Jetons d'invitation"

def is_valid(self):
def is_valid(self, user):
"""Check if the token is still valid."""
return self.user_id is None and self.valid_until >= timezone.now()

return all(
(
self.user_id is None,
self.created_by != user,
self.valid_until >= timezone.now(),
)
)


# Some data constraints checks
Expand Down
Loading
Loading