Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2a47017
feat: add space
TomLrs Mar 5, 2026
36ba311
feat: add space (#6)
tuturd Mar 5, 2026
f1498f3
update : page tickets
TomLrs Mar 6, 2026
646390a
Merge branch 'dev' into modification-page-ticket
tuturd Mar 7, 2026
3a5e018
fix: members order & feminine prefix
tuturd Mar 8, 2026
5e424fc
update: clean unused TODO tags
tuturd Mar 8, 2026
76aa44f
fix: remove hardcoded useful contacts
tuturd Mar 8, 2026
5ad6b81
feat: fill BDE legal ids
tuturd Mar 8, 2026
4d094f0
fix(try): migrations in CI tests
tuturd Mar 8, 2026
1779d4a
fix: adjust waiting time for mariadb and db name
tuturd Mar 8, 2026
00cfda8
feat: database migration
tuturd Mar 8, 2026
567e52c
revert(fix: db name)
tuturd Mar 8, 2026
5627a90
fix: remove useless print
tuturd Mar 8, 2026
5c4e65b
feat: pre-commit checks & requirements update
tuturd Mar 8, 2026
a97ed13
Initial plan
Copilot Mar 9, 2026
2d01b27
Apply review feedback: fix grammar, use partials/title.html, convert …
Copilot Mar 9, 2026
1c0b16c
feat: SEO improvement
tuturd Mar 23, 2026
9493cca
feat: logging (can be disable)
tuturd Mar 24, 2026
474605a
Feat/logging (#11)
tuturd Mar 24, 2026
9971bb1
feat: OIDC groups mapping
tuturd Mar 25, 2026
449abf0
Merge branch 'dev' into feat/OIDC-groups-mapping
tuturd Mar 25, 2026
cefc9c9
feat: OIDC groups mapping (#12)
tuturd Mar 25, 2026
c5cfd19
fix: Refactor user creation and update logic
tuturd Mar 25, 2026
9662943
fix: Add create_user method to auth_backends.py
tuturd Mar 25, 2026
b335a4f
fix: Add _claims_context method to extract user claims from OIDC
tuturd Mar 25, 2026
8abb1b7
update: empty teams label
tuturd Mar 25, 2026
3ad2deb
Merge branch 'dev' into fix/members-sorting
tuturd Mar 25, 2026
c7b2858
fix(tickets): apply review feedback — semantic HTML, grammar/typo fix…
tuturd Mar 27, 2026
9f61fcd
update: remodel some lists
tuturd Mar 27, 2026
3074f54
Merge branch 'dev' into modification-page-ticket
tuturd Mar 27, 2026
1138147
update : page tickets (#8)
tuturd Mar 27, 2026
5b0cd41
refactor: remove construction card from event and home templates
tuturd Mar 27, 2026
1c35256
Merge branch 'dev' into fix/members-sorting
tuturd Mar 27, 2026
d92f0a2
Fix/members sorting & SEO (#9)
tuturd Mar 27, 2026
6097edf
Merge branch 'master' into dev
tuturd Mar 27, 2026
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
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,15 @@ OIDC_OP_JWKS_ENDPOINT=
OIDC_RP_CLIENT_ID=
OIDC_RP_CLIENT_SECRET=
OIDC_RP_SIGN_ALGO= # example: RS256
OIDC_RP_SCOPES= # example: openid email profile
OIDC_RP_SCOPES= # example: openid email profile
OIDC_SUPERUSER_GROUP= # OIDC group giving Django staff+superuser
OIDC_EDITOR_GROUP= # OIDC group granting staff + editor Django group
OIDC_EDITOR_DJANGO_GROUP= # Django group name (example: Editeur BDE)

# Logging / debugging
DJANGO_LOG_LEVEL= # example: DEBUG, INFO, WARNING, ERROR
OIDC_LOG_LEVEL= # example: DEBUG, INFO
GUNICORN_LOG_LEVEL= # example: debug, info, warning, error
GUNICORN_WORKERS= # example: 2
GUNICORN_THREADS= # example: 2
GUNICORN_TIMEOUT= # example: 120
14 changes: 10 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,9 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Check for missing migrations
run: python manage.py makemigrations --check --dry-run

- name: Wait for MariaDB
run: |
for i in {1..30}; do
for i in {1..60}; do
if mysqladmin ping -h127.0.0.1 -utestuser -ptestpass --silent; then
echo "MariaDB is up!"
exit 0
Expand All @@ -74,6 +71,15 @@ jobs:
echo "DB_PORT=3306" >> $GITHUB_ENV
echo "SECRET_KEY=testsecrret" >> $GITHUB_ENV

- name: Check for missing migrations
env:
DJANGO_DB_NAME: ${{ env.DB_NAME }}
DJANGO_DB_USER: ${{ env.DB_USER }}
DJANGO_DB_PASSWORD: ${{ env.DB_PASSWORD }}
DJANGO_DB_HOST: ${{ env.DB_HOST }}
DJANGO_DB_PORT: ${{ env.DB_PORT }}
run: python manage.py makemigrations --check --dry-run

- name: Run Django tests
env:
DJANGO_DB_NAME: ${{ env.DB_NAME }}
Expand Down
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: local
hooks:
- id: django-migrations-check
name: Check Django migrations
entry: .venv/bin/python manage.py makemigrations --check --dry-run
language: system
pass_filenames: false
- id: django-tests
name: Run Django tests
entry: .venv/bin/python manage.py test --no-input
language: system
pass_filenames: false
- id: lint
name: Run linters
entry: .venv/bin/python -m mypy --config-file mypy.ini .
language: system
pass_filenames: false
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["sh", "-c", "python manage.py migrate && python -m bde.generate_robots && gunicorn bde.wsgi:application --bind 0.0.0.0:8000"]
CMD ["sh", "-c", "python manage.py migrate && python -m bde.generate_robots && gunicorn -c bde/gunicorn.conf.py bde.wsgi:application"]
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,16 @@ mypy .
docker compose logs -f web
```

## Pre-commit

```bash
# depuis un shell avec le venv actif
pre-commit install
pre-commit run --all-files
```

Les hooks locaux utilisent le Python du projet (`.venv/bin/python`) pour les checks Django et `mypy`.

## Arborescence principale

- `bde/` : configuration Django (settings, urls, env)
Expand Down
143 changes: 128 additions & 15 deletions auth/auth_backends.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,142 @@
# You're lucky, I added some comments to this file to explain the custom logic.
# Maybe because it's not trivial (that's the least we can say...)
# So, have a good read !
# Arthur

# Some imports (trivial for now)
import logging

from django.conf import settings
from django.contrib.auth.models import Group
from mozilla_django_oidc.auth import ( # type: ignore[import-untyped]
OIDCAuthenticationBackend,
)


logger = logging.getLogger(__name__)


class CustomOIDCBackend(OIDCAuthenticationBackend):

# Normalize the groups claim so downstream checks always work on a list[str].
@staticmethod
def _extract_groups(claims):
groups = claims.get("groups", [])
if isinstance(groups, str):
return [groups]
if isinstance(groups, list):
return [str(group) for group in groups]
logger.warning("OIDC claims 'groups' has unsupported type: %s", type(groups))
return []

@staticmethod
def _claims_context(claims):
return {
"sub": claims.get("sub"),
"preferred_username": claims.get("preferred_username"),
"email": claims.get("email"),
"name": claims.get("name"),
}

# Synchronize Django access flags and group membership from OIDC claims.
# Rules:
# - superuser comes only from OIDC_SUPERUSER_GROUP
# - staff comes from superuser OR editor OIDC group
# - editor Django group membership mirrors editor OIDC group presence
def _sync_oidc_access(self, user, claims):
oidc_groups = self._extract_groups(claims)

# Names are configured through environment-backed Django settings.
superuser_group = getattr(settings, "OIDC_SUPERUSER_GROUP", "")
editor_group = getattr(settings, "OIDC_EDITOR_GROUP", "")
editor_django_group_name = getattr(settings, "OIDC_EDITOR_DJANGO_GROUP", "")

is_superuser_from_oidc = (
bool(superuser_group) and superuser_group in oidc_groups
)
has_editor_from_oidc = bool(editor_group) and editor_group in oidc_groups

if not superuser_group:
logger.warning(
"OIDC_SUPERUSER_GROUP is not configured; superuser flag will be removed"
)

# Keep Django privilege flags aligned with OIDC source of truth.
user.is_superuser = is_superuser_from_oidc
user.is_staff = is_superuser_from_oidc or has_editor_from_oidc

if not editor_django_group_name:
logger.warning(
"OIDC_EDITOR_DJANGO_GROUP is not configured; cannot grant editor group"
)
return

try:
django_editor_group = Group.objects.get(name=editor_django_group_name)
except Group.DoesNotExist:
logger.warning(
"Configured Django editor group '%s' does not exist",
editor_django_group_name,
)
return

if not editor_group:
logger.warning(
"OIDC_EDITOR_GROUP is not configured; removing editor Django group"
)

# Add or remove the editor Django group on every login to avoid drift.
if has_editor_from_oidc:
user.groups.add(django_editor_group)
else:
user.groups.remove(django_editor_group)

# First login path: create local user, then apply profile and access sync.
def create_user(self, claims):
user = super().create_user(claims)
logger.debug("OIDC create_user start: %s", self._claims_context(claims))
try:
user = super().create_user(claims)

name = claims.get("name", user.username)
name = claims.get("name", user.username)

user.username = claims.get("preferred_username", user.username)
user.first_name = name.split(" ")[0] if " " in name else name
user.last_name = " ".join(name.split(" ")[1:]) if " " in name else ""
user.email = claims.get("email", "")
user.save()
user.username = claims.get("preferred_username", user.username)
user.first_name = name.split(" ")[0] if " " in name else name
user.last_name = " ".join(name.split(" ")[1:]) if " " in name else ""
user.email = claims.get("email", "")
self._sync_oidc_access(user, claims)
user.save()

return user
logger.info("OIDC create_user success for username=%s", user.username)
return user
except Exception:
logger.exception(
"OIDC create_user failed: %s", self._claims_context(claims)
)
raise

# Subsequent logins: refresh profile and access rules from OIDC.
def update_user(self, user, claims):
name = claims.get("name", user.username)
logger.debug(
"OIDC update_user start for username=%s claims=%s",
user.username,
self._claims_context(claims),
)
try:
name = claims.get("name", user.username)

user.username = claims.get("preferred_username", user.username)
user.first_name = name.split(" ")[0] if " " in name else name
user.last_name = " ".join(name.split(" ")[1:]) if " " in name else ""
user.email = claims.get("email", "")
user.save()
user.username = claims.get("preferred_username", user.username)
user.first_name = name.split(" ")[0] if " " in name else name
user.last_name = " ".join(name.split(" ")[1:]) if " " in name else ""
user.email = claims.get("email", "")
self._sync_oidc_access(user, claims)
user.save()

return user
logger.info("OIDC update_user success for username=%s", user.username)
return user
except Exception:
logger.exception(
"OIDC update_user failed for username=%s claims=%s",
user.username,
self._claims_context(claims),
)
raise
24 changes: 24 additions & 0 deletions bde/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,27 @@ def OIDC_RP_SIGN_ALGO(self) -> str:
@property
def OIDC_RP_SCOPES(self) -> str:
return self.get("OIDC_RP_SCOPES", "")

@property
def OIDC_SUPERUSER_GROUP(self) -> str:
return self.get("OIDC_SUPERUSER_GROUP", "")

@property
def OIDC_EDITOR_GROUP(self) -> str:
return self.get("OIDC_EDITOR_GROUP", "")

@property
def OIDC_EDITOR_DJANGO_GROUP(self) -> str:
return self.get("OIDC_EDITOR_DJANGO_GROUP", "")

@property
def DJANGO_LOG_LEVEL(self) -> str:
return self.get("DJANGO_LOG_LEVEL", "INFO")

@property
def OIDC_LOG_LEVEL(self) -> str:
return self.get("OIDC_LOG_LEVEL", "DEBUG")

@property
def GUNICORN_LOG_LEVEL(self) -> str:
return self.get("GUNICORN_LOG_LEVEL", "debug")
20 changes: 11 additions & 9 deletions bde/generate_robots.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@
def get_robots_content() -> str:

if env.DEV_MODE:
return "User-Agent: *\nDisallow: /"
return "User-Agent: *\nDisallow: /\nSitemap: /sitemap.xml\n"

return f"""User-agent: *
Disallow: /{env.ADMIN_URL}
Disallow: /logout/
Disallow: /redirect/
Disallow: /sso/
Disallow: /uploads/
"""
return (
f"User-agent: *\n"
f"Disallow: /{env.ADMIN_URL}\n"
"Disallow: /logout/\n"
"Disallow: /redirect/\n"
"Disallow: /sso/\n"
"Disallow: /uploads/\n"
"Sitemap: /sitemap.xml\n"
)


BASE_DIR = Path(__file__).resolve().parent.parent
STATICFILES_DIR = BASE_DIR / ("staticfiles" if not env.DEBUG else "static")
STATICFILES_DIR = BASE_DIR / "staticfiles"
STATICFILES_DIR.mkdir(exist_ok=True)

robots_path = STATICFILES_DIR / "robots.txt"
Expand Down
18 changes: 18 additions & 0 deletions bde/gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os


bind = "0.0.0.0:8000"
workers = int(os.getenv("GUNICORN_WORKERS", "2"))
threads = int(os.getenv("GUNICORN_THREADS", "2"))
timeout = int(os.getenv("GUNICORN_TIMEOUT", "120"))

accesslog = "-"
errorlog = "-"
loglevel = os.getenv("GUNICORN_LOG_LEVEL", "debug")
capture_output = True
enable_stdio_inheritance = True

access_log_format = (
'%(h)s %(l)s %(u)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" '
"rt=%(M)sms req_id=%({x-request-id}i)s fwd=%({x-forwarded-for}i)s"
)
Loading
Loading