mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 04:09:26 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b79fa14919 | |||
| c7c0df5b6d | |||
| cb00347be6 | |||
| bcb004ab4b | |||
| 422f838899 | |||
| 462c6c50e5 | |||
| 63565b38c3 | |||
| 10d759bdbb |
@@ -287,6 +287,7 @@ These are the environmental options available on meet backend.
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | oidc endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | oidc endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | |
|
||||
| OIDC_RP_SCOPES | oidc scopes | openid email |
|
||||
@@ -301,6 +302,7 @@ These are the environmental options available on meet backend.
|
||||
| OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | required claims from OIDC token | [] |
|
||||
| LIVEKIT_API_KEY | livekit api key | |
|
||||
| LIVEKIT_API_SECRET | livekit api secret | |
|
||||
| LIVEKIT_API_URL | livekit api url | |
|
||||
|
||||
@@ -120,7 +120,7 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
role = instance.get_role(request.user)
|
||||
is_admin = models.RoleChoices.check_administrator_role(role)
|
||||
|
||||
if role is not None:
|
||||
if is_admin:
|
||||
access_serializer = NestedResourceAccessSerializer(
|
||||
instance.accesses.select_related("resource", "user").all(),
|
||||
context=self.context,
|
||||
|
||||
@@ -531,40 +531,12 @@ class RoomViewSet(
|
||||
)
|
||||
|
||||
|
||||
class ResourceAccessListModelMixin:
|
||||
"""List mixin for resource access API."""
|
||||
|
||||
def get_permissions(self):
|
||||
"""User only needs to be authenticated to list rooms access"""
|
||||
if self.action == "list":
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
else:
|
||||
return super().get_permissions()
|
||||
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
queryset = super().get_queryset()
|
||||
if self.action == "list":
|
||||
user = self.request.user
|
||||
queryset = queryset.filter(
|
||||
Q(resource__accesses__user=user),
|
||||
resource__accesses__role__in=[
|
||||
models.RoleChoices.ADMIN,
|
||||
models.RoleChoices.OWNER,
|
||||
],
|
||||
).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class ResourceAccessViewSet(
|
||||
ResourceAccessListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""
|
||||
@@ -575,6 +547,25 @@ class ResourceAccessViewSet(
|
||||
queryset = models.ResourceAccess.objects.all()
|
||||
serializer_class = serializers.ResourceAccessSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Return the queryset according to the action."""
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Restrict access to resources the user either has explicit
|
||||
# permissions for or administrative privileges over.
|
||||
if self.action == "list":
|
||||
user = self.request.user
|
||||
queryset = queryset.filter(
|
||||
Q(resource__accesses__user=user),
|
||||
resource__accesses__role__in=[
|
||||
models.RoleChoices.ADMIN,
|
||||
models.RoleChoices.OWNER,
|
||||
],
|
||||
).distinct()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class RecordingViewSet(
|
||||
mixins.DestroyModelMixin,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Authentication Backends for the Meet core app."""
|
||||
|
||||
import contextlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import requests
|
||||
from mozilla_django_oidc.auth import (
|
||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||
from lasuite.oidc_login.backends import (
|
||||
OIDCAuthenticationBackend as LaSuiteOIDCAuthenticationBackend,
|
||||
)
|
||||
|
||||
from core.models import User
|
||||
@@ -17,93 +18,46 @@ from core.services.marketing import (
|
||||
)
|
||||
|
||||
|
||||
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
||||
"""Custom OpenID Connect (OIDC) Authentication Backend.
|
||||
|
||||
This class overrides the default OIDC Authentication Backend to accommodate differences
|
||||
in the User and Identity models, and handles signed and/or encrypted UserInfo response.
|
||||
"""
|
||||
|
||||
def get_userinfo(self, access_token, id_token, payload):
|
||||
"""Return user details dictionary.
|
||||
def get_extra_claims(self, user_info):
|
||||
"""
|
||||
Return extra claims from user_info.
|
||||
|
||||
Parameters:
|
||||
- access_token (str): The access token.
|
||||
- id_token (str): The id token (unused).
|
||||
- payload (dict): The token payload (unused).
|
||||
|
||||
Note: The id_token and payload parameters are unused in this implementation,
|
||||
but were kept to preserve base method signature.
|
||||
|
||||
Note: It handles signed and/or encrypted UserInfo Response. It is required by
|
||||
Agent Connect, which follows the OIDC standard. It forces us to override the
|
||||
base method, which deal with 'application/json' response.
|
||||
Args:
|
||||
user_info (dict): The user information dictionary.
|
||||
|
||||
Returns:
|
||||
- dict: User details dictionary obtained from the OpenID Connect user endpoint.
|
||||
dict: A dictionary of extra claims.
|
||||
|
||||
"""
|
||||
|
||||
user_response = requests.get(
|
||||
self.OIDC_OP_USER_ENDPOINT,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
verify=self.get_settings("OIDC_VERIFY_SSL", True),
|
||||
timeout=self.get_settings("OIDC_TIMEOUT", None),
|
||||
proxies=self.get_settings("OIDC_PROXY", None),
|
||||
)
|
||||
user_response.raise_for_status()
|
||||
userinfo = self.verify_token(user_response.text)
|
||||
return userinfo
|
||||
|
||||
def get_or_create_user(self, access_token, id_token, payload):
|
||||
"""Return a User based on userinfo. Get or create a new user if no user matches the Sub.
|
||||
|
||||
Parameters:
|
||||
- access_token (str): The access token.
|
||||
- id_token (str): The ID token.
|
||||
- payload (dict): The user payload.
|
||||
|
||||
Returns:
|
||||
- User: An existing or newly created User instance.
|
||||
|
||||
Raises:
|
||||
- Exception: Raised when user creation is not allowed and no existing user is found.
|
||||
"""
|
||||
|
||||
user_info = self.get_userinfo(access_token, id_token, payload)
|
||||
sub = user_info.get("sub")
|
||||
|
||||
if not sub:
|
||||
raise SuspiciousOperation(
|
||||
_("User info contained no recognizable user identification")
|
||||
)
|
||||
|
||||
email = user_info.get("email")
|
||||
user = self.get_existing_user(sub, email)
|
||||
|
||||
claims = {
|
||||
"email": email,
|
||||
return {
|
||||
# Get user's full name from OIDC fields defined in settings
|
||||
"full_name": self.compute_full_name(user_info),
|
||||
"short_name": user_info.get(settings.OIDC_USERINFO_SHORTNAME_FIELD),
|
||||
}
|
||||
if not user and self.get_settings("OIDC_CREATE_USER", True):
|
||||
user = User.objects.create(
|
||||
sub=sub,
|
||||
password="!", # noqa: S106
|
||||
**claims,
|
||||
)
|
||||
|
||||
if settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL:
|
||||
self.signup_to_marketing_email(email)
|
||||
def post_get_or_create_user(self, user, claims, is_new_user):
|
||||
"""
|
||||
Post-processing after user creation or retrieval.
|
||||
|
||||
elif not user:
|
||||
return None
|
||||
Args:
|
||||
user (User): The user instance.
|
||||
claims (dict): The claims dictionary.
|
||||
is_new_user (bool): Indicates if the user was newly created.
|
||||
|
||||
if not user.is_active:
|
||||
raise SuspiciousOperation(_("User account is disabled"))
|
||||
Returns:
|
||||
- None
|
||||
|
||||
self.update_user_if_needed(user, claims)
|
||||
|
||||
return user
|
||||
"""
|
||||
email = claims["email"]
|
||||
if is_new_user and email and settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL:
|
||||
self.signup_to_marketing_email(email)
|
||||
|
||||
@staticmethod
|
||||
def signup_to_marketing_email(email):
|
||||
@@ -116,7 +70,9 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
|
||||
Note: For a more robust solution, consider using Async task processing (Celery/Django-Q)
|
||||
"""
|
||||
try:
|
||||
with contextlib.suppress(
|
||||
ContactCreationError, ImproperlyConfigured, ImportError
|
||||
):
|
||||
marketing_service = get_marketing_service()
|
||||
contact_data = ContactData(
|
||||
email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]}
|
||||
@@ -124,8 +80,6 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
marketing_service.create_contact(
|
||||
contact_data, timeout=settings.BREVO_API_TIMEOUT
|
||||
)
|
||||
except (ContactCreationError, ImproperlyConfigured, ImportError):
|
||||
pass
|
||||
|
||||
def get_existing_user(self, sub, email):
|
||||
"""Fetch existing user by sub or email."""
|
||||
@@ -142,32 +96,3 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||
_("Multiple user accounts share a common email.")
|
||||
) from e
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def compute_full_name(user_info):
|
||||
"""Compute user's full name based on OIDC fields in settings."""
|
||||
full_name = " ".join(
|
||||
filter(
|
||||
None,
|
||||
(
|
||||
user_info.get(field)
|
||||
for field in settings.OIDC_USERINFO_FULLNAME_FIELDS
|
||||
),
|
||||
)
|
||||
)
|
||||
return full_name or None
|
||||
|
||||
@staticmethod
|
||||
def update_user_if_needed(user, claims):
|
||||
"""Update user claims if they have changed."""
|
||||
user_fields = vars(user.__class__) # Get available model fields
|
||||
updated_claims = {
|
||||
key: value
|
||||
for key, value in claims.items()
|
||||
if value and key in user_fields and value != getattr(user, key)
|
||||
}
|
||||
|
||||
if not updated_claims:
|
||||
return
|
||||
|
||||
User.objects.filter(sub=user.sub).update(**updated_claims)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
"""Authentication URLs for the People core app."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls
|
||||
|
||||
from .views import OIDCLogoutCallbackView, OIDCLogoutView
|
||||
|
||||
urlpatterns = [
|
||||
# Override the default 'logout/' path from Mozilla Django OIDC with our custom view.
|
||||
path("logout/", OIDCLogoutView.as_view(), name="oidc_logout_custom"),
|
||||
path(
|
||||
"logout-callback/",
|
||||
OIDCLogoutCallbackView.as_view(),
|
||||
name="oidc_logout_callback",
|
||||
),
|
||||
*mozzila_oidc_urls,
|
||||
]
|
||||
@@ -1,181 +0,0 @@
|
||||
"""Authentication Views for the People core app."""
|
||||
|
||||
import copy
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils import crypto
|
||||
|
||||
from mozilla_django_oidc.utils import (
|
||||
absolutify,
|
||||
)
|
||||
from mozilla_django_oidc.views import (
|
||||
OIDCAuthenticationCallbackView as MozillaOIDCAuthenticationCallbackView,
|
||||
)
|
||||
from mozilla_django_oidc.views import (
|
||||
OIDCAuthenticationRequestView as MozillaOIDCAuthenticationRequestView,
|
||||
)
|
||||
from mozilla_django_oidc.views import (
|
||||
OIDCLogoutView as MozillaOIDCOIDCLogoutView,
|
||||
)
|
||||
|
||||
|
||||
class OIDCLogoutView(MozillaOIDCOIDCLogoutView):
|
||||
"""Custom logout view for handling OpenID Connect (OIDC) logout flow.
|
||||
|
||||
Adds support for handling logout callbacks from the identity provider (OP)
|
||||
by initiating the logout flow if the user has an active session.
|
||||
|
||||
The Django session is retained during the logout process to persist the 'state' OIDC parameter.
|
||||
This parameter is crucial for maintaining the integrity of the logout flow between this call
|
||||
and the subsequent callback.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def persist_state(request, state):
|
||||
"""Persist the given 'state' parameter in the session's 'oidc_states' dictionary
|
||||
|
||||
This method is used to store the OIDC state parameter in the session, according to the
|
||||
structure expected by Mozilla Django OIDC's 'add_state_and_verifier_and_nonce_to_session'
|
||||
utility function.
|
||||
"""
|
||||
|
||||
if "oidc_states" not in request.session or not isinstance(
|
||||
request.session["oidc_states"], dict
|
||||
):
|
||||
request.session["oidc_states"] = {}
|
||||
|
||||
request.session["oidc_states"][state] = {}
|
||||
request.session.save()
|
||||
|
||||
def construct_oidc_logout_url(self, request):
|
||||
"""Create the redirect URL for interfacing with the OIDC provider.
|
||||
|
||||
Retrieves the necessary parameters from the session and constructs the URL
|
||||
required to initiate logout with the OpenID Connect provider.
|
||||
|
||||
If no ID token is found in the session, the logout flow will not be initiated,
|
||||
and the method will return the default redirect URL.
|
||||
|
||||
The 'state' parameter is generated randomly and persisted in the session to ensure
|
||||
its integrity during the subsequent callback.
|
||||
"""
|
||||
|
||||
oidc_logout_endpoint = self.get_settings("OIDC_OP_LOGOUT_ENDPOINT")
|
||||
|
||||
if not oidc_logout_endpoint:
|
||||
return self.redirect_url
|
||||
|
||||
reverse_url = reverse("oidc_logout_callback")
|
||||
id_token = request.session.get("oidc_id_token", None)
|
||||
|
||||
if not id_token:
|
||||
return self.redirect_url
|
||||
|
||||
query = {
|
||||
"id_token_hint": id_token,
|
||||
"state": crypto.get_random_string(self.get_settings("OIDC_STATE_SIZE", 32)),
|
||||
"post_logout_redirect_uri": absolutify(request, reverse_url),
|
||||
}
|
||||
|
||||
self.persist_state(request, query["state"])
|
||||
|
||||
return f"{oidc_logout_endpoint}?{urlencode(query)}"
|
||||
|
||||
def post(self, request):
|
||||
"""Handle user logout.
|
||||
|
||||
If the user is not authenticated, redirects to the default logout URL.
|
||||
Otherwise, constructs the OIDC logout URL and redirects the user to start
|
||||
the logout process.
|
||||
|
||||
If the user is redirected to the default logout URL, ensure her Django session
|
||||
is terminated.
|
||||
"""
|
||||
|
||||
logout_url = self.redirect_url
|
||||
|
||||
if request.user.is_authenticated:
|
||||
logout_url = self.construct_oidc_logout_url(request)
|
||||
|
||||
# If the user is not redirected to the OIDC provider, ensure logout
|
||||
if logout_url == self.redirect_url:
|
||||
auth.logout(request)
|
||||
|
||||
return HttpResponseRedirect(logout_url)
|
||||
|
||||
|
||||
class OIDCLogoutCallbackView(MozillaOIDCOIDCLogoutView):
|
||||
"""Custom view for handling the logout callback from the OpenID Connect (OIDC) provider.
|
||||
|
||||
Handles the callback after logout from the identity provider (OP).
|
||||
Verifies the state parameter and performs necessary logout actions.
|
||||
|
||||
The Django session is maintained during the logout process to ensure the integrity
|
||||
of the logout flow initiated in the previous step.
|
||||
"""
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get(self, request):
|
||||
"""Handle the logout callback.
|
||||
|
||||
If the user is not authenticated, redirects to the default logout URL.
|
||||
Otherwise, verifies the state parameter and performs necessary logout actions.
|
||||
"""
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(self.redirect_url)
|
||||
|
||||
state = request.GET.get("state")
|
||||
|
||||
if state not in request.session.get("oidc_states", {}):
|
||||
msg = "OIDC callback state not found in session `oidc_states`!"
|
||||
raise SuspiciousOperation(msg)
|
||||
|
||||
del request.session["oidc_states"][state]
|
||||
request.session.save()
|
||||
|
||||
auth.logout(request)
|
||||
|
||||
return HttpResponseRedirect(self.redirect_url)
|
||||
|
||||
|
||||
class OIDCAuthenticationCallbackView(MozillaOIDCAuthenticationCallbackView):
|
||||
"""Custom callback view for handling the silent login flow."""
|
||||
|
||||
@property
|
||||
def failure_url(self):
|
||||
"""Override the failure URL property to handle silent login flow
|
||||
|
||||
A silent login failure (e.g., no active user session) should not be
|
||||
considered as an authentication failure.
|
||||
"""
|
||||
if self.request.session.get("silent", None):
|
||||
del self.request.session["silent"]
|
||||
self.request.session.save()
|
||||
return self.success_url
|
||||
return super().failure_url
|
||||
|
||||
|
||||
class OIDCAuthenticationRequestView(MozillaOIDCAuthenticationRequestView):
|
||||
"""Custom authentication view for handling the silent login flow."""
|
||||
|
||||
def get_extra_params(self, request):
|
||||
"""Handle 'prompt' extra parameter for the silent login flow
|
||||
|
||||
This extra parameter is necessary to distinguish between a standard
|
||||
authentication flow and the silent login flow.
|
||||
"""
|
||||
extra_params = self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", None)
|
||||
if extra_params is None:
|
||||
extra_params = {}
|
||||
if request.GET.get("silent") == "true":
|
||||
extra_params = copy.deepcopy(extra_params)
|
||||
extra_params.update({"prompt": "none"})
|
||||
request.session["silent"] = True
|
||||
request.session.save()
|
||||
return extra_params
|
||||
@@ -41,6 +41,10 @@ class BaseEgressService:
|
||||
raise WorkerConnectionError(
|
||||
f"LiveKit client connection error, {e.message}."
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise WorkerConnectionError(
|
||||
f"Unexpected error during LiveKit client connection: {str(e)}"
|
||||
) from e
|
||||
|
||||
finally:
|
||||
await lkapi.aclose()
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||
|
||||
assert user.sub == "123"
|
||||
assert user.email is None
|
||||
assert user.password == "!"
|
||||
assert user.has_usable_password() is False
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
|
||||
assert user.email == email
|
||||
assert user.full_name is None
|
||||
assert user.short_name is None
|
||||
assert user.password == "!"
|
||||
assert user.has_usable_password() is False
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def test_authentication_getter_new_user_with_names(monkeypatch, email):
|
||||
assert user.email == email
|
||||
assert user.full_name == "John Doe"
|
||||
assert user.short_name == "John"
|
||||
assert user.password == "!"
|
||||
assert user.has_usable_password() is False
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_models_oidc_user_getter_invalid_token(django_assert_num_queries, monkey
|
||||
django_assert_num_queries(0),
|
||||
pytest.raises(
|
||||
SuspiciousOperation,
|
||||
match="User info contained no recognizable user identification",
|
||||
match="Claims verification failed",
|
||||
),
|
||||
):
|
||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Unit tests for the Authentication URLs."""
|
||||
|
||||
from core.authentication.urls import urlpatterns
|
||||
|
||||
|
||||
def test_urls_override_default_mozilla_django_oidc():
|
||||
"""Custom URL patterns should override default ones from Mozilla Django OIDC."""
|
||||
|
||||
url_names = [u.name for u in urlpatterns]
|
||||
assert url_names.index("oidc_logout_custom") < url_names.index("oidc_logout")
|
||||
@@ -1,359 +0,0 @@
|
||||
"""Unit tests for the Authentication Views."""
|
||||
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import crypto
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
from core.authentication.views import (
|
||||
MozillaOIDCAuthenticationCallbackView,
|
||||
OIDCAuthenticationCallbackView,
|
||||
OIDCAuthenticationRequestView,
|
||||
OIDCLogoutCallbackView,
|
||||
OIDCLogoutView,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_anonymous():
|
||||
"""Anonymous users calling the logout url,
|
||||
should be redirected to the specified LOGOUT_REDIRECT_URL."""
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
response = APIClient().get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
OIDCLogoutView, "construct_oidc_logout_url", return_value="/example-logout"
|
||||
)
|
||||
def test_view_logout(mocked_oidc_logout_url):
|
||||
"""Authenticated users should be redirected to OIDC provider for logout."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
response = client.get(url)
|
||||
|
||||
mocked_oidc_logout_url.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/default-redirect-logout")
|
||||
@mock.patch.object(
|
||||
OIDCLogoutView, "construct_oidc_logout_url", return_value="/default-redirect-logout"
|
||||
)
|
||||
def test_view_logout_no_oidc_provider(mocked_oidc_logout_url):
|
||||
"""Authenticated users should be logged out when no OIDC provider is available."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
url = reverse("oidc_logout_custom")
|
||||
|
||||
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
|
||||
response = client.get(url)
|
||||
mocked_oidc_logout_url.assert_called_once()
|
||||
mock_logout.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/default-redirect-logout"
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_callback_anonymous():
|
||||
"""Anonymous users calling the logout callback url,
|
||||
should be redirected to the specified LOGOUT_REDIRECT_URL."""
|
||||
|
||||
url = reverse("oidc_logout_callback")
|
||||
response = APIClient().get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_oidc_states",
|
||||
[{}, {"other_state": "foo"}],
|
||||
)
|
||||
def test_view_logout_persist_state(initial_oidc_states):
|
||||
"""State value should be persisted in session's data."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
if initial_oidc_states:
|
||||
request.session["oidc_states"] = initial_oidc_states
|
||||
request.session.save()
|
||||
|
||||
mocked_state = "mock_state"
|
||||
|
||||
OIDCLogoutView().persist_state(request, mocked_state)
|
||||
|
||||
assert "oidc_states" in request.session
|
||||
assert request.session["oidc_states"] == {
|
||||
"mock_state": {},
|
||||
**initial_oidc_states,
|
||||
}
|
||||
|
||||
|
||||
@override_settings(OIDC_OP_LOGOUT_ENDPOINT="/example-logout")
|
||||
@mock.patch.object(OIDCLogoutView, "persist_state")
|
||||
@mock.patch.object(crypto, "get_random_string", return_value="mocked_state")
|
||||
def test_view_logout_construct_oidc_logout_url(
|
||||
mocked_get_random_string, mocked_persist_state
|
||||
):
|
||||
"""Should construct the logout URL to initiate the logout flow with the OIDC provider."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
request.session["oidc_id_token"] = "mocked_oidc_id_token"
|
||||
request.session.save()
|
||||
|
||||
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
|
||||
|
||||
mocked_persist_state.assert_called_once()
|
||||
mocked_get_random_string.assert_called_once()
|
||||
|
||||
params = parse_qs(urlparse(redirect_url).query)
|
||||
|
||||
assert params["id_token_hint"][0] == "mocked_oidc_id_token"
|
||||
assert params["state"][0] == "mocked_state"
|
||||
|
||||
url = reverse("oidc_logout_callback")
|
||||
assert url in params["post_logout_redirect_uri"][0]
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/")
|
||||
def test_view_logout_construct_oidc_logout_url_none_id_token():
|
||||
"""If no ID token is available in the session,
|
||||
the user should be redirected to the final URL."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
redirect_url = OIDCLogoutView().construct_oidc_logout_url(request)
|
||||
|
||||
assert redirect_url == "/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_state",
|
||||
[None, {"other_state": "foo"}],
|
||||
)
|
||||
def test_view_logout_callback_wrong_state(initial_state):
|
||||
"""Should raise an error if OIDC state doesn't match session data."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
if initial_state:
|
||||
request.session["oidc_states"] = initial_state
|
||||
request.session.save()
|
||||
|
||||
callback_view = OIDCLogoutCallbackView.as_view()
|
||||
|
||||
with pytest.raises(SuspiciousOperation) as excinfo:
|
||||
callback_view(request)
|
||||
|
||||
assert (
|
||||
str(excinfo.value) == "OIDC callback state not found in session `oidc_states`!"
|
||||
)
|
||||
|
||||
|
||||
@override_settings(LOGOUT_REDIRECT_URL="/example-logout")
|
||||
def test_view_logout_callback():
|
||||
"""If state matches, callback should clear OIDC state and redirects."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().get("/logout-callback/", data={"state": "mocked_state"})
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
mocked_state = "mocked_state"
|
||||
|
||||
request.session["oidc_states"] = {mocked_state: {}}
|
||||
request.session.save()
|
||||
|
||||
callback_view = OIDCLogoutCallbackView.as_view()
|
||||
|
||||
with mock.patch("mozilla_django_oidc.views.auth.logout") as mock_logout:
|
||||
|
||||
def clear_user(request):
|
||||
# Assert state is cleared prior to logout
|
||||
assert request.session["oidc_states"] == {}
|
||||
request.user = AnonymousUser()
|
||||
|
||||
mock_logout.side_effect = clear_user
|
||||
response = callback_view(request)
|
||||
mock_logout.assert_called_once()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == "/example-logout"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None])
|
||||
def test_view_authentication_default(settings, mocked_extra_params_setting):
|
||||
"""By default, authentication request should not trigger silent login."""
|
||||
|
||||
settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
request.GET = {}
|
||||
|
||||
view = OIDCAuthenticationRequestView()
|
||||
extra_params = view.get_extra_params(request)
|
||||
|
||||
assert extra_params == (mocked_extra_params_setting or {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None])
|
||||
def test_view_authentication_silent_false(settings, mocked_extra_params_setting):
|
||||
"""Ensure setting 'silent' parameter to a random value doesn't trigger the silent login flow."""
|
||||
|
||||
settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
request.GET = {"silent": "foo"}
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
view = OIDCAuthenticationRequestView()
|
||||
extra_params = view.get_extra_params(request)
|
||||
|
||||
assert extra_params == (mocked_extra_params_setting or {})
|
||||
assert not request.session.get("silent")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mocked_extra_params_setting", [{"foo": 123}, {}, None])
|
||||
def test_view_authentication_silent_true(settings, mocked_extra_params_setting):
|
||||
"""If 'silent' parameter is set to True, the silent login should be triggered."""
|
||||
settings.OIDC_AUTH_REQUEST_EXTRA_PARAMS = mocked_extra_params_setting
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
request.GET = {"silent": "true"}
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
view = OIDCAuthenticationRequestView()
|
||||
extra_params = view.get_extra_params(request)
|
||||
expected_params = {"prompt": "none"}
|
||||
|
||||
assert (
|
||||
extra_params == {**mocked_extra_params_setting, **expected_params}
|
||||
if mocked_extra_params_setting
|
||||
else expected_params
|
||||
)
|
||||
assert request.session.get("silent") is True
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
MozillaOIDCAuthenticationCallbackView,
|
||||
"failure_url",
|
||||
new_callable=mock.PropertyMock,
|
||||
return_value="foo",
|
||||
)
|
||||
def test_view_callback_failure_url(mocked_failure_url):
|
||||
"""Test default behavior of the 'failure_url' property"""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
view = OIDCAuthenticationCallbackView()
|
||||
view.request = request
|
||||
|
||||
returned_url = view.failure_url
|
||||
|
||||
mocked_failure_url.assert_called_once()
|
||||
assert returned_url == "foo"
|
||||
|
||||
|
||||
@mock.patch.object(
|
||||
OIDCAuthenticationCallbackView,
|
||||
"success_url",
|
||||
new_callable=mock.PropertyMock,
|
||||
return_value="foo",
|
||||
)
|
||||
def test_view_callback_failure_url_silent_login(mocked_success_url):
|
||||
"""If a silent login was initiated and failed, it should not be treated as a failure."""
|
||||
|
||||
user = factories.UserFactory()
|
||||
|
||||
request = RequestFactory().request()
|
||||
request.user = user
|
||||
|
||||
middleware = SessionMiddleware(get_response=lambda x: x)
|
||||
middleware.process_request(request)
|
||||
|
||||
request.session["silent"] = True
|
||||
request.session.save()
|
||||
|
||||
view = OIDCAuthenticationCallbackView()
|
||||
view.request = request
|
||||
|
||||
returned_url = view.failure_url
|
||||
|
||||
mocked_success_url.assert_called_once()
|
||||
assert returned_url == "foo"
|
||||
assert not request.session.get("silent")
|
||||
@@ -338,22 +338,20 @@ def test_api_rooms_retrieve_authenticated():
|
||||
)
|
||||
def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, settings):
|
||||
"""
|
||||
Users who are members of a room should be allowed to see related users.
|
||||
Users who are members of a room should not be allowed to see related users.
|
||||
"""
|
||||
settings.TIME_ZONE = "UTC"
|
||||
user = UserFactory()
|
||||
other_user = UserFactory()
|
||||
|
||||
room = RoomFactory()
|
||||
user_access = UserResourceAccessFactory(resource=room, user=user, role="member")
|
||||
other_user_access = UserResourceAccessFactory(
|
||||
resource=room, user=other_user, role="member"
|
||||
)
|
||||
UserResourceAccessFactory(resource=room, user=user, role="member")
|
||||
UserResourceAccessFactory(resource=room, user=other_user, role="member")
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(3):
|
||||
response = client.get(
|
||||
f"/api/v1.0/rooms/{room.id!s}/",
|
||||
)
|
||||
@@ -361,37 +359,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
|
||||
assert response.status_code == 200
|
||||
content_dict = response.json()
|
||||
|
||||
assert sorted(content_dict.pop("accesses"), key=lambda x: x["id"]) == sorted(
|
||||
[
|
||||
{
|
||||
"id": str(user_access.id),
|
||||
"user": {
|
||||
"id": str(user_access.user.id),
|
||||
"email": user_access.user.email,
|
||||
"full_name": user_access.user.full_name,
|
||||
"short_name": user_access.user.short_name,
|
||||
"timezone": "UTC",
|
||||
"language": user_access.user.language,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": user_access.role,
|
||||
},
|
||||
{
|
||||
"id": str(other_user_access.id),
|
||||
"user": {
|
||||
"id": str(other_user_access.user.id),
|
||||
"email": other_user_access.user.email,
|
||||
"full_name": other_user_access.user.full_name,
|
||||
"short_name": other_user_access.user.short_name,
|
||||
"timezone": "UTC",
|
||||
"language": other_user_access.user.language,
|
||||
},
|
||||
"resource": str(room.id),
|
||||
"role": other_user_access.role,
|
||||
},
|
||||
],
|
||||
key=lambda x: x["id"],
|
||||
)
|
||||
assert "accesses" not in content_dict
|
||||
|
||||
expected_name = str(room.id)
|
||||
assert content_dict == {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from django.conf import settings
|
||||
from django.urls import include, path
|
||||
|
||||
from lasuite.oidc_login.urls import urlpatterns as oidc_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from core.api import get_frontend_configuration, viewsets
|
||||
from core.authentication.urls import urlpatterns as oidc_urls
|
||||
|
||||
# - Main endpoints
|
||||
router = DefaultRouter()
|
||||
|
||||
@@ -263,6 +263,9 @@ class Base(Configuration):
|
||||
"rest_framework.parsers.JSONParser",
|
||||
"nested_multipart_parser.drf.DrfNestedParser",
|
||||
],
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
],
|
||||
"EXCEPTION_HANDLER": "core.api.exception_handler",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
@@ -359,8 +362,8 @@ class Base(Configuration):
|
||||
SESSION_COOKIE_AGE = 60 * 60 * 12
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_AUTHENTICATE_CLASS = "core.authentication.views.OIDCAuthenticationRequestView"
|
||||
OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView"
|
||||
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
|
||||
OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView"
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_CREATE_USER", environ_prefix=None
|
||||
)
|
||||
@@ -394,6 +397,9 @@ class Base(Configuration):
|
||||
OIDC_OP_USER_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_USER_ENDPOINT_FORMAT = values.Value(
|
||||
"AUTO", environ_name="OIDC_OP_USER_ENDPOINT_FORMAT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_LOGOUT_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
@@ -440,6 +446,11 @@ class Base(Configuration):
|
||||
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_USERINFO_ESSENTIAL_CLAIMS = values.ListValue(
|
||||
default=[],
|
||||
environ_name="OIDC_USERINFO_ESSENTIAL_CLAIMS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Video conference configuration
|
||||
LIVEKIT_CONFIGURATION = {
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -32,6 +32,7 @@ dependencies = [
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-lasuite==0.0.7",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==5.4.0",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"dependencies": {
|
||||
"@livekit/components-react": "2.8.1",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
|
||||
@@ -27,5 +27,6 @@ export function useStartRecording(
|
||||
return useMutation<ApiRoom, ApiError, StartRecordingParams>({
|
||||
mutationFn: startRecording,
|
||||
onSuccess: options?.onSuccess,
|
||||
onError: options?.onError,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ export function useStopRecording(
|
||||
return useMutation<ApiRoom, ApiError, StopRecordingParams>({
|
||||
mutationFn: stopRecording,
|
||||
onSuccess: options?.onSuccess,
|
||||
onError: options?.onError,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { A, Button, Div, H, Text } from '@/primitives'
|
||||
import { A, Button, Dialog, Div, H, P, Text } from '@/primitives'
|
||||
|
||||
import fourthSlide from '@/assets/intro-slider/4_record.png'
|
||||
import { css } from '@/styled-system/css'
|
||||
@@ -29,12 +29,20 @@ export const ScreenRecordingSidePanel = () => {
|
||||
const recordingSnap = useSnapshot(recordingStore)
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
|
||||
|
||||
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
|
||||
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
|
||||
const roomId = useRoomId()
|
||||
|
||||
const { mutateAsync: startRecordingRoom } = useStartRecording()
|
||||
const { mutateAsync: stopRecordingRoom } = useStopRecording()
|
||||
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
|
||||
useStartRecording({
|
||||
onError: () => setIsErrorDialogOpen('start'),
|
||||
})
|
||||
const { mutateAsync: stopRecordingRoom, isPending: isPendingToStop } =
|
||||
useStopRecording({
|
||||
onError: () => setIsErrorDialogOpen('stop'),
|
||||
})
|
||||
|
||||
const statuses = useMemo(() => {
|
||||
return {
|
||||
@@ -145,7 +153,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{statuses.isStopping ? (
|
||||
{statuses.isStopping || isPendingToStop ? (
|
||||
<>
|
||||
<H lvl={3} margin={false}>
|
||||
{t('stopping.heading')}
|
||||
@@ -193,7 +201,7 @@ export const ScreenRecordingSidePanel = () => {
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
>
|
||||
{statuses.isStarting ? (
|
||||
{statuses.isStarting || isPendingToStart ? (
|
||||
<>
|
||||
<Spinner size={20} />
|
||||
{t('start.loading')}
|
||||
@@ -206,6 +214,20 @@ export const ScreenRecordingSidePanel = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
isOpen={!!isErrorDialogOpen}
|
||||
role="alertdialog"
|
||||
aria-label={t('alert.title')}
|
||||
>
|
||||
<P>{t(`alert.body.${isErrorDialogOpen}`)}</P>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onPress={() => setIsErrorDialogOpen('')}
|
||||
>
|
||||
{t('alert.button')}
|
||||
</Button>
|
||||
</Dialog>
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { A, Button, Div, H, LinkButton, Text } from '@/primitives'
|
||||
import { A, Button, Dialog, Div, H, LinkButton, P, Text } from '@/primitives'
|
||||
|
||||
import thirdSlide from '@/assets/intro-slider/3_resume.png'
|
||||
import { css } from '@/styled-system/css'
|
||||
@@ -32,6 +32,8 @@ export const TranscriptSidePanel = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
|
||||
|
||||
const [isErrorDialogOpen, setIsErrorDialogOpen] = useState('')
|
||||
|
||||
const recordingSnap = useSnapshot(recordingStore)
|
||||
|
||||
const { notifyParticipants } = useNotifyParticipants()
|
||||
@@ -42,8 +44,15 @@ export const TranscriptSidePanel = () => {
|
||||
)
|
||||
const roomId = useRoomId()
|
||||
|
||||
const { mutateAsync: startRecordingRoom } = useStartRecording()
|
||||
const { mutateAsync: stopRecordingRoom } = useStopRecording()
|
||||
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
|
||||
useStartRecording({
|
||||
onError: () => setIsErrorDialogOpen('start'),
|
||||
})
|
||||
|
||||
const { mutateAsync: stopRecordingRoom, isPending: isPendingToStop } =
|
||||
useStopRecording({
|
||||
onError: () => setIsErrorDialogOpen('stop'),
|
||||
})
|
||||
|
||||
const statuses = useMemo(() => {
|
||||
return {
|
||||
@@ -177,7 +186,7 @@ export const TranscriptSidePanel = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{statuses.isStopping ? (
|
||||
{statuses.isStopping || isPendingToStop ? (
|
||||
<>
|
||||
<H lvl={3} margin={false}>
|
||||
{t('stopping.heading')}
|
||||
@@ -225,7 +234,7 @@ export const TranscriptSidePanel = () => {
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
>
|
||||
{statuses.isStarting ? (
|
||||
{statuses.isStarting || isPendingToStart ? (
|
||||
<>
|
||||
<Spinner size={20} />
|
||||
{t('start.loading')}
|
||||
@@ -240,6 +249,20 @@ export const TranscriptSidePanel = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
isOpen={!!isErrorDialogOpen}
|
||||
role="alertdialog"
|
||||
aria-label={t('alert.title')}
|
||||
>
|
||||
<P>{t(`alert.body.${isErrorDialogOpen}`)}</P>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
onPress={() => setIsErrorDialogOpen('')}
|
||||
>
|
||||
{t('alert.button')}
|
||||
</Button>
|
||||
</Dialog>
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -237,6 +237,14 @@
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": ""
|
||||
},
|
||||
"alert": {
|
||||
"title": "",
|
||||
"body": {
|
||||
"stop": "",
|
||||
"start": ""
|
||||
},
|
||||
"button": ""
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
@@ -254,6 +262,14 @@
|
||||
"heading": "",
|
||||
"body": "",
|
||||
"button": ""
|
||||
},
|
||||
"alert": {
|
||||
"title": "",
|
||||
"body": {
|
||||
"stop": "",
|
||||
"start": ""
|
||||
},
|
||||
"button": ""
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -237,6 +237,14 @@
|
||||
"heading": "Become a beta tester",
|
||||
"body": "Record your meeting for later. You will receive a summary by email once the meeting is finished.",
|
||||
"button": "Sign up"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Transcription Failed",
|
||||
"body": {
|
||||
"stop": "We were unable to stop the transcription. Please try again in a moment.",
|
||||
"start": "We were unable to start the transcription. Please try again in a moment."
|
||||
},
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
@@ -255,6 +263,14 @@
|
||||
"heading": "Recording in progress…",
|
||||
"body": "You will receive the result by email once the recording is complete.",
|
||||
"button": "Stop recording"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Recording Failed",
|
||||
"body": {
|
||||
"stop": "We were unable to stop the recording. Please try again in a moment.",
|
||||
"start": "We were unable to start the recording. Please try again in a moment."
|
||||
},
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -237,6 +237,14 @@
|
||||
"heading": "Devenez beta testeur",
|
||||
"body": "Enregistrer votre réunion pour plus tard. Vous recevrez un compte-rendu par email une fois la réunion terminée.",
|
||||
"button": "Inscrivez-vous"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Échec de transcription",
|
||||
"body": {
|
||||
"stop": "Nous n'avons pas pu stopper la transcription. Veuillez réessayer dans quelques instants.",
|
||||
"start": "Nous n'avons pas pu démarrer la transcription. Veuillez réessayer dans quelques instants."
|
||||
},
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
@@ -255,6 +263,14 @@
|
||||
"heading": "Enregistrement en cours …",
|
||||
"body": "Vous recevrez le resultat par email une fois l'enregistrement terminé.",
|
||||
"button": "Arrêter l'enregistrement"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Échec de l'enregistrement",
|
||||
"body": {
|
||||
"stop": "Nous n'avons pas pu stopper l'enregistrement. Veuillez réessayer dans quelques instants.",
|
||||
"start": "Nous n'avons pas pu démarrer l'enregistrement. Veuillez réessayer dans quelques instants."
|
||||
},
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
@@ -237,6 +237,14 @@
|
||||
"heading": "Word betatester",
|
||||
"body": "Neem uw vergadering op voor later. U ontvangt een samenvatting per e-mail zodra de vergadering is afgelopen.",
|
||||
"button": "Aanmelden"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Transcriptie mislukt",
|
||||
"body": {
|
||||
"stop": "We konden de transcriptie niet stoppen. Probeer het over enkele ogenblikken opnieuw.",
|
||||
"start": "We konden de transcriptie niet starten. Probeer het over enkele ogenblikken opnieuw."
|
||||
},
|
||||
"button": "Opnieuw proberen"
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
@@ -255,6 +263,14 @@
|
||||
"heading": "Opname bezig …",
|
||||
"body": "Je ontvangt het resultaat per e-mail zodra de opname is voltooid.",
|
||||
"button": "Opname stoppen"
|
||||
},
|
||||
"alert": {
|
||||
"title": "Opname mislukt",
|
||||
"body": {
|
||||
"stop": "We konden de opname niet stoppen. Probeer het over enkele ogenblikken opnieuw.",
|
||||
"start": "We konden de opname niet starten. Probeer het over enkele ogenblikken opnieuw."
|
||||
},
|
||||
"button": "Opnieuw proberen"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"type": "module",
|
||||
"main": "./dist/visio-sdk.umd.cjs",
|
||||
"module": "./dist/visio-sdk.js",
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.21",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
@@ -620,7 +620,7 @@
|
||||
},
|
||||
"library": {
|
||||
"name": "@gouvfr-lasuite/visio-sdk",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.21",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
Reference in New Issue
Block a user