Compare commits

..

6 Commits

Author SHA1 Message Date
lebaudantoine 1306f0bcfe (backend) expose is_active field for Application in Django admin
Add minimal admin support to allow administrators to mark an
application account as inactive if it becomes rogue or compromised.

This capability was missing when the application concept was
initially introduced in the backend.
2026-03-12 13:27:08 +01:00
lebaudantoine ca4494c09e ♻️(backend) align Application model field with is_active convention
Most models in the project use `is_active` rather than `active`.
The Application model was not aligned with this convention.

Rename the field and add a migration to standardize the naming.
Update related tests accordingly.

This change should not introduce breaking changes for external
applications.
2026-03-12 13:26:37 +01:00
lebaudantoine f57db0acc8 ♻️(backend) refactor uploaded file's key format
Remove the user-provided filename while preserving the extension,
and switch to the format `uuid.extension`.

This is more natural than the previous `uuid/.extension` format
and avoids confusion.

Update related tests accordingly.
2026-03-12 10:39:59 +01:00
Florent Chehab 342f992556 🔨(python-env) migrate meet main app to UV
Migrate main meet app to use UV for dependancy management.
Also optimized the backend image build sequence for faster rebuilds
when dependencies don't change.
Also removed compiled django translations files are they are done in the build
process now.

Changes inspired by drive repo.
2026-03-12 10:25:35 +01:00
lebaudantoine d0cf3974ad 🚸(frontend) allow downloading recordings with "failed to stop" status
When support manually marks a recording as "failed to stop," enable
users to download the recording from the frontend.

This ensures users can recover their recordings even if the
automatic stop process fails.
2026-03-11 19:41:45 +01:00
lebaudantoine 0f3eb35c83 🩹(backend) add Django admin action to update recording status manually
Making the status read-only improved security and enforced least
privilege, but in production some recordings could not be properly
stopped, leaving them in an active state.

Introduce an admin action to allow support staff to mark recordings
as "failed to stop" or update their status when necessary.
2026-03-11 19:41:45 +01:00
29 changed files with 2484 additions and 76 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ __pycache__
**/__pycache__
**/*.pyc
venv
.venv
**/.venv
# System-specific files
.DS_Store
+14 -12
View File
@@ -124,15 +124,17 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install the project
run: uv sync --locked --all-extras
- name: Check code formatting with ruff
run: ~/.local/bin/ruff format . --diff
run: uv run ruff format . --diff
- name: Lint code with ruff
run: ~/.local/bin/ruff check .
run: uv run ruff check .
- name: Lint code with pylint
run: ~/.local/bin/pylint meet demo core
run: uv run pylint meet demo core
lint-agents:
runs-on: ubuntu-latest
@@ -279,10 +281,10 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install the dependencies
run: uv sync --locked --all-extras
- name: Install gettext (required to compile messages)
run: |
@@ -290,10 +292,10 @@ jobs:
sudo apt-get install -y gettext
- name: Generate a MO file from strings extracted from the project
run: python manage.py compilemessages
run: uv run python manage.py compilemessages
- name: Run tests
run: ~/.local/bin/pytest -n 2
run: uv run pytest -n 2
lint-front:
runs-on: ubuntu-latest
+1
View File
@@ -31,6 +31,7 @@ MANIFEST
# Translations # Translations
*.pot
*.mo
# Environments
.env
+3
View File
@@ -13,6 +13,7 @@ and this project adheres to
- ✨(helm) support celery with our Django backend #1124
- ✨(helm) support ingress for custom background image #1124
- ✨(backend) add authenticated user rate throttling on request-entry #1129
- ✨(backend) expose `is_active` field for Application in Django admin #1133
### Changed
@@ -22,6 +23,8 @@ and this project adheres to
- ♿(frontend) improve chat toast a11y for screen readers #1109
- ♿(frontend) improve ui and aria labels for help article links #1108
- 🌐(frontend) improve German translation #1125
- 🔨(python-env) migrate meet main app to UV #1120
- ♻️(backend) align Application model field with `is_active` convention #1133
### Fixed
+39 -23
View File
@@ -13,14 +13,28 @@ RUN apk update && \
# ---- Back-end builder image ----
FROM base AS back-builder
WORKDIR /builder
# Copy required python dependencies
COPY ./src/backend /builder
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
RUN mkdir /install && \
pip install --prefix=/install .
# Disable Python downloads, because we want to use the system interpreter
# across both images. If using a managed Python version, it needs to be
# copied from the build image into the final image;
ENV UV_PYTHON_DOWNLOADS=0
# install uv
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=src/backend/uv.lock,target=uv.lock \
--mount=type=bind,source=src/backend/pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev
COPY src/backend /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
# ---- mails ----
FROM node:20 AS mail-builder
@@ -30,7 +44,7 @@ COPY ./src/mail /mail/app
WORKDIR /mail/app
RUN yarn install --frozen-lockfile && \
yarn build
yarn build
# ---- static link collector ----
@@ -42,17 +56,17 @@ RUN apk add \
libmagic \
rdfind
# Copy installed python dependencies
COPY --from=back-builder /install /usr/local
# Copy Meet application (see .dockerignore)
COPY ./src/backend /app/
WORKDIR /app
# Copy the application from the builder
COPY --from=back-builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
# collectstatic
RUN DJANGO_CONFIGURATION=Build DJANGO_JWT_PRIVATE_SIGNING_KEY=Dummy \
python manage.py collectstatic --noinput
python manage.py collectstatic --noinput
# Replace duplicated file by a symlink to decrease the overall size of the
# final image
@@ -81,14 +95,17 @@ COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
# docker user (see entrypoint).
RUN chmod g=u /etc/passwd
# Copy installed python dependencies
COPY --from=back-builder /install /usr/local
# Copy Meet application (see .dockerignore)
COPY ./src/backend /app/
# Copy the application from the builder
COPY --from=back-builder /app /app
WORKDIR /app
ENV PATH="/app/.venv/bin:$PATH"
# Generate compiled translation messages
RUN DJANGO_CONFIGURATION=Build \
python manage.py compilemessages --ignore=".venv/**/*"
# We wrap commands run in this container by the following entrypoint that
# creates a user on-the-fly with the container user ID (see USER) and root group
# ID.
@@ -103,10 +120,9 @@ USER root:root
# Install psql
RUN apk add postgresql-client
# Uninstall Meet and re-install it in editable mode along with development
# dependencies
RUN pip uninstall -y meet
RUN pip install -e .[dev]
# Install development dependencies
RUN --mount=from=ghcr.io/astral-sh/uv:0.10.9,source=/uv,target=/bin/uv \
uv sync --all-extras --locked
# Restore the un-privileged user running the application
ARG DOCKER_USER
@@ -115,7 +131,7 @@ USER ${DOCKER_USER}
# Target database host (e.g. database engine following docker compose services
# name) & port
ENV DB_HOST=postgresql \
DB_PORT=5432
DB_PORT=5432
# Run django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
+1 -1
View File
@@ -223,7 +223,7 @@ superuser: ## Create an admin superuser with password "admin"
.PHONY: superuser
back-i18n-compile: ## compile the gettext files
@$(MANAGE) compilemessages --ignore="venv/**/*"
@$(MANAGE) compilemessages --ignore=".venv/**/*"
.PHONY: back-i18n-compile
back-i18n-generate: ## create the .pot files used for i18n
+2
View File
@@ -80,6 +80,7 @@ services:
volumes:
- ./src/backend:/app
- ./data/static:/data/static
- /app/.venv
depends_on:
- postgresql
- mailcatcher
@@ -105,6 +106,7 @@ services:
volumes:
- ./src/backend:/app
- ./data/static:/data/static
- /app/.venv
depends_on:
- app-dev
+1 -1
View File
@@ -71,7 +71,7 @@ backend:
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volume to manage our local custom CA and avoid to set ssl_verify: false
+2 -1
View File
@@ -308,7 +308,7 @@ class ApplicationAdmin(admin.ModelAdmin):
form = ApplicationAdminForm
list_display = ("id", "name", "client_id", "get_scopes_display")
list_display = ("id", "name", "client_id", "get_scopes_display", "is_active")
fields = [
"name",
"id",
@@ -317,6 +317,7 @@ class ApplicationAdmin(admin.ModelAdmin):
"scopes",
"client_id",
"client_secret",
"is_active",
]
readonly_fields = ["id", "created_at", "updated_at"]
inlines = [ApplicationDomainInline]
+1 -1
View File
@@ -85,7 +85,7 @@ UUID_REGEX = (
FILE_EXT_REGEX = r"[\d\w]+"
MEDIA_STORAGE_URL_PATTERN = re.compile(
f"{settings.MEDIA_URL:s}"
rf"(?P<key>{FILE_FOLDER:s}/(?P<pk>{UUID_REGEX:s})/\.{FILE_EXT_REGEX:s})$"
rf"(?P<key>{FILE_FOLDER:s}/(?P<pk>{UUID_REGEX:s})\.{FILE_EXT_REGEX:s})$"
)
@@ -203,7 +203,7 @@ class ApplicationJWTAuthentication(BaseJWTAuthentication):
logger.warning("Application not found: %s", client_id)
raise exceptions.AuthenticationFailed("Application not found.") from e
if not application.active:
if not application.is_active:
logger.warning(
"Inactive application attempted authentication: %s", client_id
)
+1 -1
View File
@@ -61,7 +61,7 @@ class ApplicationViewSet(viewsets.ViewSet):
except models.Application.DoesNotExist as e:
raise drf_exceptions.AuthenticationFailed("Invalid credentials") from e
if not application.active:
if not application.is_active:
raise drf_exceptions.AuthenticationFailed("Application is inactive")
if not check_password(client_secret, application.client_secret):
+1 -1
View File
@@ -129,7 +129,7 @@ class ApplicationFactory(factory.django.DjangoModelFactory):
model = models.Application
name = factory.Faker("company")
active = True
is_active = True
client_id = factory.LazyFunction(utils.generate_client_id)
client_secret = factory.LazyFunction(utils.generate_client_secret)
scopes = []
@@ -0,0 +1,18 @@
# Generated by Django 5.2.12 on 2026-03-11 14:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0017_file'),
]
operations = [
migrations.RenameField(
model_name='application',
old_name='active',
new_name='is_active',
),
]
+2 -2
View File
@@ -759,7 +759,7 @@ class Application(BaseModel):
verbose_name=_("Application name"),
help_text=_("Descriptive name for this application."),
)
active = models.BooleanField(default=True)
is_active = models.BooleanField(default=True)
client_id = models.CharField(
max_length=100, unique=True, default=utils.generate_client_id
)
@@ -952,7 +952,7 @@ class File(BaseModel):
_, extension = splitext(self.filename)
# We store only the extension in the storage system to avoid
# leaking Personal Information in logs, etc.
return f"{self.key_base}/{extension!s}"
return f"{self.key_base}{extension!s}"
def get_abilities(self, user):
"""
@@ -118,7 +118,7 @@ def test_api_files_create_file_authenticated_success():
assert policy_parsed.scheme == "http"
assert policy_parsed.netloc == "localhost:9000"
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}/.png"
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
query_params = parse_qs(policy_parsed.query)
@@ -39,7 +39,7 @@ def test_api_files_update_anonymous_forbidden():
def test_api_files_update_description_and_title():
"""
Test the description and title of an file can be updated.
Test the description and title of a file can be updated.
"""
user = factories.UserFactory()
@@ -904,7 +904,7 @@ def test_api_rooms_token_unknown_application(settings):
def test_api_rooms_token_inactive_application(settings):
"""Token for inactive application should be rejected."""
application = ApplicationFactory(active=False)
application = ApplicationFactory(is_active=False)
now = datetime.now(timezone.utc)
payload = {
@@ -23,7 +23,7 @@ def test_api_applications_generate_token_success(settings):
"""Valid credentials should return a JWT token."""
UserFactory(email="User.Family@example.com")
application = ApplicationFactory(
active=True,
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
)
@@ -79,7 +79,7 @@ def test_api_applications_generate_token_invalid_client_id():
def test_api_applications_generate_token_invalid_client_secret():
"""Invalid client_secret should return 401."""
user = UserFactory(email="user@example.com")
application = ApplicationFactory(active=True)
application = ApplicationFactory(is_active=True)
client = APIClient()
response = client.post(
@@ -100,7 +100,7 @@ def test_api_applications_generate_token_invalid_client_secret():
def test_api_applications_generate_token_inactive_application():
"""Inactive application should return 401."""
user = UserFactory(email="user@example.com")
application = ApplicationFactory(active=False)
application = ApplicationFactory(is_active=False)
plain_secret = "test-secret-123"
application.client_secret = plain_secret
@@ -124,7 +124,7 @@ def test_api_applications_generate_token_inactive_application():
def test_api_applications_generate_token_invalid_email_format():
"""Invalid email format should return 400."""
application = ApplicationFactory(active=True)
application = ApplicationFactory(is_active=True)
plain_secret = "test-secret-123"
application.client_secret = plain_secret
@@ -149,7 +149,7 @@ def test_api_applications_generate_token_invalid_email_format():
def test_api_applications_generate_token_domain_not_authorized():
"""Application without domain authorization should return 403."""
user = UserFactory(email="user@denied.com")
application = ApplicationFactory(active=True)
application = ApplicationFactory(is_active=True)
ApplicationDomainFactory(application=application, domain="allowed.com")
plain_secret = "test-secret-123"
@@ -176,7 +176,7 @@ def test_api_applications_generate_token_domain_authorized():
"""Application with domain authorization should succeed."""
user = UserFactory(email="user@allowed.com")
application = ApplicationFactory(
active=True,
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST],
)
ApplicationDomainFactory(application=application, domain="allowed.com")
@@ -203,7 +203,7 @@ def test_api_applications_generate_token_domain_authorized():
def test_api_applications_generate_token_user_not_found():
"""Non-existent user should return 404."""
application = ApplicationFactory(active=True)
application = ApplicationFactory(is_active=True)
plain_secret = "test-secret-123"
application.client_secret = plain_secret
@@ -231,7 +231,7 @@ def test_api_applications_token_payload_structure(settings):
user = UserFactory(email="user@example.com")
application = ApplicationFactory(
active=True,
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
)
@@ -284,7 +284,7 @@ def test_api_applications_token_new_user(settings):
assert len(User.objects.all()) == 0
application = ApplicationFactory(
active=True,
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
)
@@ -342,7 +342,7 @@ def test_api_applications_token_existing_user(settings):
assert len(User.objects.all()) == 1
application = ApplicationFactory(
active=True,
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
)
@@ -41,7 +41,7 @@ def test_models_application_name_maxlength():
def test_models_application_active_default():
"""An application should be active by default."""
application = Application.objects.create(name="Test App")
assert application.active is True
assert application.is_active is True
def test_models_application_scopes_default():
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -11
View File
@@ -2,8 +2,8 @@
# Meet package
#
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
requires = ["uv_build>=0.10.9,<0.11.0"]
build-backend = "uv_build"
[project]
name = "meet"
@@ -21,8 +21,7 @@ classifiers = [
]
description = "A simple video and phone conferencing tool, powered by LiveKit"
keywords = ["Django", "Contacts", "Templates", "RBAC"]
license = { file = "LICENSE" }
readme = "README.md"
license = "MIT"
requires-python = ">=3.13"
dependencies = [
"boto3==1.42.49",
@@ -70,7 +69,7 @@ dependencies = [
"Homepage" = "https://github.com/suitenumerique/meet"
"Repository" = "https://github.com/suitenumerique/meet"
[project.optional-dependencies]
[dependency-groups]
dev = [
"django-extensions==4.1",
"drf-spectacular-sidecar==2026.1.1",
@@ -90,12 +89,13 @@ dev = [
"types-requests==2.32.4.20260107",
]
[tool.setuptools]
packages = { find = { where = ["."], exclude = ["tests"] } }
zip-safe = true
[tool.distutils.bdist_wheel]
universal = true
[tool.uv.build-backend]
module-root = ""
source-exclude = [
"**/tests/**",
"**/test_*.py",
"**/tests.py",
]
[tool.ruff]
exclude = [
+2365
View File
File diff suppressed because it is too large Load Diff
@@ -71,7 +71,7 @@ backend:
SUMMARY_SERVICE_API_TOKEN: password
RECORDING_DOWNLOAD_BASE_URL: https://meet.127.0.0.1.nip.io/recording
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
migrate:
@@ -100,7 +100,7 @@ backend:
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
@@ -75,7 +75,7 @@ backend:
ROOM_TELEPHONY_ENABLED: True
ROOM_TELEPHONY_DEFAULT_COUNTRY: 'FR'
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
ROOM_SUBTITLE_ENABLED: True
EXTERNAL_API_ENABLED: True
APPLICATION_JWT_AUDIENCE: https://meet.127.0.0.1.nip.io/external-api/v1.0/
@@ -113,7 +113,7 @@ backend:
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
+2 -2
View File
@@ -94,7 +94,7 @@ backend:
key: BREVO_API_KEY
BREVO_API_CONTACT_LIST_IDS: 8
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
migrate:
@@ -123,7 +123,7 @@ backend:
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /app/.venv/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false