✨(back) check on document update if user can save it
When a document is updated, users not connected to the collaboration server can override work made by other people connected to the collaboration server. To avoid this, the priority is given to user connected to the collaboration server. If the websocket property in the request payload is missing or set to False, the backend fetch the collaboration server to now if the user can save or not. If users are already connected, the user can't save. Also, only one user without websocket can save a connect, the first user saving acquire a lock and all other users can't save. To implement this behavior, we need to track all users, connected and not, so a session is created for every user in the ForceSessionMiddleware.
This commit is contained in:
@@ -15,9 +15,11 @@ and this project adheres to
|
|||||||
- 📝(project) add troubleshoot doc #1066
|
- 📝(project) add troubleshoot doc #1066
|
||||||
- 📝(project) add system-requirement doc #1066
|
- 📝(project) add system-requirement doc #1066
|
||||||
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
|
- 🔧(front) configure x-frame-options to DENY in nginx conf #1084
|
||||||
- (doc) add documentation to install with compose #855
|
|
||||||
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
|
- ✨(backend) allow to disable checking unsafe mimetype on attachment upload
|
||||||
- ✨Ask for access #1081
|
- ✨Ask for access #1081
|
||||||
|
- ✨(doc) add documentation to install with compose #855
|
||||||
|
- ✨ Give priority to users connected to collaboration server
|
||||||
|
(aka no websocket feature) #1093
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
196
docs/env.md
196
docs/env.md
@@ -6,104 +6,104 @@ Here we describe all environment variables that can be set for the docs applicat
|
|||||||
|
|
||||||
These are the environment variables you can set for the `impress-backend` container.
|
These are the environment variables you can set for the `impress-backend` container.
|
||||||
|
|
||||||
| Option | Description | default |
|
| Option | Description | default |
|
||||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||||
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
|
||||||
| AI_API_KEY | AI key to be used for AI Base url | |
|
| AI_API_KEY | AI key to be used for AI Base url | |
|
||||||
| AI_BASE_URL | OpenAI compatible AI base url | |
|
| AI_BASE_URL | OpenAI compatible AI base url | |
|
||||||
| AI_FEATURE_ENABLED | Enable AI options | false |
|
| AI_FEATURE_ENABLED | Enable AI options | false |
|
||||||
| AI_MODEL | AI Model to use | |
|
| AI_MODEL | AI Model to use | |
|
||||||
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
|
||||||
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
| API_USERS_LIST_LIMIT | Limit on API users | 5 |
|
||||||
| API_USERS_LIST_THROTTLE_RATE_BURST | throttle rate for api on burst | 30/minute |
|
| API_USERS_LIST_THROTTLE_RATE_BURST | Throttle rate for api on burst | 30/minute |
|
||||||
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | throttle rate for api | 180/hour |
|
| API_USERS_LIST_THROTTLE_RATE_SUSTAINED | Throttle rate for api | 180/hour |
|
||||||
| AWS_S3_ACCESS_KEY_ID | access id for s3 endpoint | |
|
| AWS_S3_ACCESS_KEY_ID | Access id for s3 endpoint | |
|
||||||
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
|
||||||
| AWS_S3_REGION_NAME | region name for s3 endpoint | |
|
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
|
||||||
| AWS_S3_SECRET_ACCESS_KEY | access key for s3 endpoint | |
|
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
|
||||||
| AWS_STORAGE_BUCKET_NAME | bucket name for s3 endpoint | impress-media-storage |
|
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
|
||||||
| CACHES_DEFAULT_TIMEOUT | cache default timeout | 30 |
|
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
|
||||||
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||||
| COLLABORATION_API_URL | collaboration api host | |
|
| COLLABORATION_API_URL | Collaboration api host | |
|
||||||
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
|
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
|
||||||
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
|
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
|
||||||
| COLLABORATION_WS_URL | collaboration websocket url | |
|
| COLLABORATION_WS_URL | Collaboration websocket url | |
|
||||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
|
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
|
||||||
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
| CONVERSION_API_SECURE | Require secure conversion api | false |
|
||||||
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
| CONVERSION_API_TIMEOUT | Conversion api timeout | 30 |
|
||||||
| CONTENT_SECURITY_POLICY_DIRECTIVES | A dict of directives set in the Content-Security-Policy header | All directives are set to 'none' |
|
| CRISP_WEBSITE_ID | Crisp website id for support | |
|
||||||
| CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES | Url with this prefix will not have the header Content-Security-Policy included | |
|
| DB_ENGINE | Engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
||||||
| CRISP_WEBSITE_ID | crisp website id for support | |
|
| DB_HOST | Host of the database | localhost |
|
||||||
| DB_ENGINE | engine to use for database connections | django.db.backends.postgresql_psycopg2 |
|
| DB_NAME | Name of the database | impress |
|
||||||
| DB_HOST | host of the database | localhost |
|
| DB_PASSWORD | Password to authenticate with | pass |
|
||||||
| DB_NAME | name of the database | impress |
|
| DB_PORT | Port of the database | 5432 |
|
||||||
| DB_PASSWORD | password to authenticate with | pass |
|
| DB_USER | User to authenticate with | dinum |
|
||||||
| DB_PORT | port of the database | 5432 |
|
| DJANGO_ALLOWED_HOSTS | Allowed hosts | [] |
|
||||||
| DB_USER | user to authenticate with | dinum |
|
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker transport options | {} |
|
||||||
| DJANGO_ALLOWED_HOSTS | allowed hosts | [] |
|
| DJANGO_CELERY_BROKER_URL | Celery broker url | redis://redis:6379/0 |
|
||||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker transport options | {} |
|
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||||
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
|
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | List of origins allowed for CORS using regulair expressions | [] |
|
||||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all CORS origins | false |
|
| DJANGO_CORS_ALLOWED_ORIGINS | List of origins allowed for CORS | [] |
|
||||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | list of origins allowed for CORS using regulair expressions | [] |
|
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
||||||
| DJANGO_CORS_ALLOWED_ORIGINS | list of origins allowed for CORS | [] |
|
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||||
| DJANGO_CSRF_TRUSTED_ORIGINS | CSRF trusted origins | [] |
|
| DJANGO_EMAIL_BRAND_NAME | Brand name for email | |
|
||||||
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
|
| DJANGO_EMAIL_FROM | Email address used as sender | from@example.com |
|
||||||
| DJANGO_EMAIL_BRAND_NAME | brand name for email | |
|
| DJANGO_EMAIL_HOST | Hostname of email | |
|
||||||
| DJANGO_EMAIL_FROM | email address used as sender | from@example.com |
|
| DJANGO_EMAIL_HOST_PASSWORD | Password to authenticate with on the email host | |
|
||||||
| DJANGO_EMAIL_HOST | host name of email | |
|
| DJANGO_EMAIL_HOST_USER | User to authenticate with on the email host | |
|
||||||
| DJANGO_EMAIL_HOST_PASSWORD | password to authenticate with on the email host | |
|
| DJANGO_EMAIL_LOGO_IMG | Logo for the email | |
|
||||||
| DJANGO_EMAIL_HOST_USER | user to authenticate with on the email host | |
|
| DJANGO_EMAIL_PORT | Port used to connect to email host | |
|
||||||
| DJANGO_EMAIL_LOGO_IMG | logo for the email | |
|
| DJANGO_EMAIL_USE_SSL | Use ssl for email host connection | false |
|
||||||
| DJANGO_EMAIL_PORT | port used to connect to email host | |
|
| DJANGO_EMAIL_USE_TLS | Use tls for email host connection | false |
|
||||||
| DJANGO_EMAIL_USE_SSL | use sstl for email host connection | false |
|
| DJANGO_SECRET_KEY | Secret key | |
|
||||||
| DJANGO_EMAIL_USE_TLS | use tls for email host connection | false |
|
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
||||||
| DJANGO_SECRET_KEY | secret key | |
|
| DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 |
|
||||||
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
|
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
||||||
| DOCUMENT_IMAGE_MAX_SIZE | maximum size of document in bytes | 10485760 |
|
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false |
|
||||||
| FRONTEND_CSS_URL | To add a external css file to the app | |
|
| FRONTEND_THEME | Frontend theme to use | |
|
||||||
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
|
| LANGUAGE_CODE | Default language | en-us |
|
||||||
| FRONTEND_THEME | frontend theme to use | |
|
| LOGGING_LEVEL_LOGGERS_APP | Application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||||
| LANGUAGE_CODE | default language | en-us |
|
| LOGGING_LEVEL_LOGGERS_ROOT | Default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
||||||
| LOGGING_LEVEL_LOGGERS_APP | application logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
| LOGIN_REDIRECT_URL | Login redirect url | |
|
||||||
| LOGGING_LEVEL_LOGGERS_ROOT | default logging level. options are "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" | INFO |
|
| LOGIN_REDIRECT_URL_FAILURE | Login redirect url on failure | |
|
||||||
| LOGIN_REDIRECT_URL | login redirect url | |
|
| LOGOUT_REDIRECT_URL | Logout redirect url | |
|
||||||
| LOGIN_REDIRECT_URL_FAILURE | login redirect url on failure | |
|
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
||||||
| LOGOUT_REDIRECT_URL | logout redirect url | |
|
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||||
| MALWARE_DETECTION_BACKEND | The malware detection backend use from the django-lasuite package | lasuite.malware_detection.backends.dummy.DummyBackend |
|
| MEDIA_BASE_URL | | |
|
||||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
|
||||||
| MEDIA_BASE_URL | | |
|
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
| OIDC_CREATE_USER | Create used on OIDC | false |
|
||||||
| OIDC_CREATE_USER | create used on OIDC | false |
|
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | true |
|
||||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | faillback to email for identification | true |
|
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
|
||||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | Authorization endpoint for OIDC | |
|
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
||||||
| OIDC_OP_JWKS_ENDPOINT | JWKS endpoint for OIDC | |
|
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
||||||
| OIDC_OP_LOGOUT_ENDPOINT | Logout endpoint for OIDC | |
|
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
||||||
| OIDC_OP_TOKEN_ENDPOINT | Token endpoint for OIDC | |
|
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
||||||
| OIDC_OP_USER_ENDPOINT | User endpoint for OIDC | |
|
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
||||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed hosts for OIDC redirect url | [] |
|
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
||||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require https for OIDC redirect url | false |
|
| OIDC_RP_CLIENT_ID | Client id used for OIDC | impress |
|
||||||
| OIDC_RP_CLIENT_ID | client id used for OIDC | impress |
|
| OIDC_RP_CLIENT_SECRET | Client secret used for OIDC | |
|
||||||
| OIDC_RP_CLIENT_SECRET | client secret used for OIDC | |
|
| OIDC_RP_SCOPES | Scopes requested for OIDC | openid email |
|
||||||
| OIDC_RP_SCOPES | scopes requested for OIDC | openid email |
|
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
|
||||||
| OIDC_RP_SIGN_ALGO | verification algorithm used OIDC tokens | RS256 |
|
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
||||||
| OIDC_STORE_ID_TOKEN | Store OIDC token | true |
|
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||||
| OIDC_USE_NONCE | use nonce for OIDC | true |
|
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
||||||
| OIDC_USERINFO_FULLNAME_FIELDS | OIDC token claims to create full name | ["first_name", "last_name"] |
|
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
||||||
| OIDC_USERINFO_SHORTNAME_FIELD | OIDC token claims to create shortname | first_name |
|
| POSTHOG_KEY | Posthog key for analytics | |
|
||||||
| POSTHOG_KEY | posthog key for analytics | |
|
| REDIS_URL | Cache url | redis://redis:6379/1 |
|
||||||
| REDIS_URL | cache url | redis://redis:6379/1 |
|
| SENTRY_DSN | Sentry host | |
|
||||||
| SENTRY_DSN | sentry host | |
|
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
|
||||||
| SESSION_COOKIE_AGE | duration of the cookie session | 60*60*12 |
|
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
||||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | | false |
|
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||||
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
||||||
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
|
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
||||||
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
|
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
|
||||||
| TRASHBIN_CUTOFF_DAYS | trashbin cutoff | 30 |
|
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
|
||||||
| USER_OIDC_ESSENTIAL_CLAIMS | essential claims in OIDC token | [] |
|
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
||||||
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
|
| Y_PROVIDER_API_KEY | Y provider API key | |
|
||||||
| Y_PROVIDER_API_KEY | Y provider API key | |
|
|
||||||
|
|
||||||
## impress-frontend image
|
## impress-frontend image
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"""Serialize documents with all fields for display in detail views."""
|
"""Serialize documents with all fields for display in detail views."""
|
||||||
|
|
||||||
content = serializers.CharField(required=False)
|
content = serializers.CharField(required=False)
|
||||||
|
websocket = serializers.BooleanField(required=False, write_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Document
|
model = models.Document
|
||||||
@@ -260,6 +261,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
|||||||
"title",
|
"title",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"user_roles",
|
"user_roles",
|
||||||
|
"websocket",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from rest_framework import filters, status, viewsets
|
|||||||
from rest_framework import response as drf_response
|
from rest_framework import response as drf_response
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.throttling import UserRateThrottle
|
from rest_framework.throttling import UserRateThrottle
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
from core import authentication, enums, models
|
from core import authentication, enums, models
|
||||||
from core.services.ai_services import AIService
|
from core.services.ai_services import AIService
|
||||||
@@ -634,6 +635,54 @@ class DocumentViewSet(
|
|||||||
"""Override to implement a soft delete instead of dumping the record in database."""
|
"""Override to implement a soft delete instead of dumping the record in database."""
|
||||||
instance.soft_delete()
|
instance.soft_delete()
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
"""Check rules about collaboration."""
|
||||||
|
if serializer.validated_data.get("websocket"):
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection_info = CollaborationService().get_document_connection_info(
|
||||||
|
serializer.instance.id,
|
||||||
|
self.request.session.session_key,
|
||||||
|
)
|
||||||
|
except requests.HTTPError as e:
|
||||||
|
capture_exception(e)
|
||||||
|
connection_info = {
|
||||||
|
"count": 0,
|
||||||
|
"exists": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if connection_info["count"] == 0:
|
||||||
|
# No websocket mode
|
||||||
|
logger.debug("update without connection found in the websocket server")
|
||||||
|
cache_key = f"docs:no-websocket:{serializer.instance.id}"
|
||||||
|
current_editor = cache.get(cache_key)
|
||||||
|
if not current_editor:
|
||||||
|
cache.set(
|
||||||
|
cache_key,
|
||||||
|
self.request.session.session_key,
|
||||||
|
settings.NO_WEBSOCKET_CACHE_TIMEOUT,
|
||||||
|
)
|
||||||
|
elif current_editor != self.request.session.session_key:
|
||||||
|
raise drf.exceptions.PermissionDenied(
|
||||||
|
"You are not allowed to edit this document."
|
||||||
|
)
|
||||||
|
cache.touch(cache_key, settings.NO_WEBSOCKET_CACHE_TIMEOUT)
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
if connection_info["exists"]:
|
||||||
|
# Websocket mode
|
||||||
|
logger.debug("session key found in the websocket server")
|
||||||
|
return super().perform_update(serializer)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Users connected to the websocket but current editor not connected to it. Can not edit."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise drf.exceptions.PermissionDenied(
|
||||||
|
"You are not allowed to edit this document."
|
||||||
|
)
|
||||||
|
|
||||||
@drf.decorators.action(
|
@drf.decorators.action(
|
||||||
detail=False,
|
detail=False,
|
||||||
methods=["get"],
|
methods=["get"],
|
||||||
|
|||||||
21
src/backend/core/middleware.py
Normal file
21
src/backend/core/middleware.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Force session creation for all requests."""
|
||||||
|
|
||||||
|
|
||||||
|
class ForceSessionMiddleware:
|
||||||
|
"""
|
||||||
|
Force session creation for unauthenticated users.
|
||||||
|
Must be used after Authentication middleware.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, get_response):
|
||||||
|
"""Initialize the middleware."""
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
"""Force session creation for unauthenticated users."""
|
||||||
|
|
||||||
|
if not request.user.is_authenticated and request.session.session_key is None:
|
||||||
|
request.session.create()
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
@@ -41,3 +41,31 @@ class CollaborationService:
|
|||||||
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
|
f"Failed to notify WebSocket server. Status code: {response.status_code}, "
|
||||||
f"Response: {response.text}"
|
f"Response: {response.text}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_document_connection_info(self, room, session_key):
|
||||||
|
"""
|
||||||
|
Get the connection info for a document.
|
||||||
|
"""
|
||||||
|
endpoint = "get-connections"
|
||||||
|
querystring = {
|
||||||
|
"room": room,
|
||||||
|
"sessionKey": session_key,
|
||||||
|
}
|
||||||
|
endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/"
|
||||||
|
|
||||||
|
headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
endpoint_url, headers=headers, params=querystring, timeout=10
|
||||||
|
)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise requests.HTTPError("Failed to get document connection info.") from e
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise requests.HTTPError(
|
||||||
|
f"Failed to get document connection info. Status code: {response.status_code}, "
|
||||||
|
f"Response: {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ Tests for Documents API endpoint in impress's core app: update
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import responses
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from core import factories, models
|
from core import factories, models
|
||||||
@@ -44,6 +46,7 @@ def test_api_documents_update_anonymous_forbidden(reach, role, via_parent):
|
|||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory()
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = APIClient().put(
|
response = APIClient().put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
@@ -90,8 +93,9 @@ def test_api_documents_update_authenticated_unrelated_forbidden(
|
|||||||
|
|
||||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory(),
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
@@ -141,8 +145,9 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
|||||||
|
|
||||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory(),
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
@@ -206,6 +211,7 @@ def test_api_documents_update_authenticated_reader(via, via_parent, mock_user_te
|
|||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory()
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
@@ -258,6 +264,7 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
|||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory()
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
@@ -287,6 +294,274 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
|||||||
assert value == new_document_values[key]
|
assert value == new_document_values[key]
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_authenticated_no_websocket(settings):
|
||||||
|
"""
|
||||||
|
When a user updates the document, not connected to the websocket and is the first to update,
|
||||||
|
the document should be updated.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_authenticated_no_websocket_user_already_editing(settings):
|
||||||
|
"""
|
||||||
|
When a user updates the document, not connected to the websocket and is not the first to update,
|
||||||
|
the document should not be updated.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
|
||||||
|
|
||||||
|
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||||
|
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_no_websocket_other_user_connected_to_websocket(settings):
|
||||||
|
"""
|
||||||
|
When a user updates the document, not connected to the websocket and another user is connected
|
||||||
|
to the websocket, the document should not be updated.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": False})
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_user_connected_to_websocket(settings):
|
||||||
|
"""
|
||||||
|
When a user updates the document, connected to the websocket, the document should be updated.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket(
|
||||||
|
settings,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
When the websocket server is unreachable, the document should be updated like if the user was
|
||||||
|
not connected to the websocket.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, status=500)
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||||
|
settings,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||||
|
If an other user is already editing, the document should not be updated.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = False
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, status=500)
|
||||||
|
|
||||||
|
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||||
|
assert ws_resp.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_api_documents_update_force_websocket_param_to_true(settings):
|
||||||
|
"""
|
||||||
|
When the websocket parameter is set to true, the document should be updated without any check.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory(with_owned_document=True)
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
session_key = client.session.session_key
|
||||||
|
|
||||||
|
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||||
|
|
||||||
|
new_document_values = serializers.DocumentSerializer(
|
||||||
|
instance=factories.DocumentFactory()
|
||||||
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
|
settings.COLLABORATION_API_URL = "http://example.com/"
|
||||||
|
settings.COLLABORATION_SERVER_SECRET = "secret-token"
|
||||||
|
endpoint_url = (
|
||||||
|
f"{settings.COLLABORATION_API_URL}get-connections/"
|
||||||
|
f"?room={document.id}&sessionKey={session_key}"
|
||||||
|
)
|
||||||
|
ws_resp = responses.get(endpoint_url, status=500)
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
|
new_document_values,
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||||
|
assert ws_resp.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams):
|
def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams):
|
||||||
"""
|
"""
|
||||||
@@ -317,6 +592,7 @@ def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_t
|
|||||||
new_document_values = serializers.DocumentSerializer(
|
new_document_values = serializers.DocumentSerializer(
|
||||||
instance=factories.DocumentFactory()
|
instance=factories.DocumentFactory()
|
||||||
).data
|
).data
|
||||||
|
new_document_values["websocket"] = True
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/v1.0/documents/{other_document.id!s}/",
|
f"/api/v1.0/documents/{other_document.id!s}/",
|
||||||
new_document_values,
|
new_document_values,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
|||||||
with django_assert_num_queries(11):
|
with django_assert_num_queries(11):
|
||||||
response = APIClient().put(
|
response = APIClient().put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
{"content": get_ydoc_with_mages(image_keys)},
|
{"content": get_ydoc_with_mages(image_keys), "websocket": True},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -63,7 +63,7 @@ def test_api_documents_update_new_attachment_keys_anonymous(django_assert_num_qu
|
|||||||
with django_assert_num_queries(7):
|
with django_assert_num_queries(7):
|
||||||
response = APIClient().put(
|
response = APIClient().put(
|
||||||
f"/api/v1.0/documents/{document.id!s}/",
|
f"/api/v1.0/documents/{document.id!s}/",
|
||||||
{"content": get_ydoc_with_mages(image_keys[:2])},
|
{"content": get_ydoc_with_mages(image_keys[:2]), "websocket": True},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ class Base(Configuration):
|
|||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"core.middleware.ForceSessionMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"dockerflow.django.middleware.DockerflowMiddleware",
|
"dockerflow.django.middleware.DockerflowMiddleware",
|
||||||
"csp.middleware.CSPMiddleware",
|
"csp.middleware.CSPMiddleware",
|
||||||
@@ -480,6 +481,7 @@ class Base(Configuration):
|
|||||||
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
|
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
|
||||||
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
|
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
SESSION_COOKIE_NAME = "docs_sessionid"
|
||||||
|
|
||||||
# OIDC - Authorization Code Flow
|
# OIDC - Authorization Code Flow
|
||||||
OIDC_CREATE_USER = values.BooleanValue(
|
OIDC_CREATE_USER = values.BooleanValue(
|
||||||
@@ -659,6 +661,12 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NO_WEBSOCKET_CACHE_TIMEOUT = values.Value(
|
||||||
|
default=120,
|
||||||
|
environ_name="NO_WEBSOCKET_CACHE_TIMEOUT",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
# We want to make it easy to log to console but by default we log production
|
# We want to make it easy to log to console but by default we log production
|
||||||
# to Sentry and don't want to log to console.
|
# to Sentry and don't want to log to console.
|
||||||
@@ -853,15 +861,9 @@ class Development(Base):
|
|||||||
CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"]
|
CSRF_TRUSTED_ORIGINS = ["http://localhost:8072", "http://localhost:3000"]
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = "impress_sessionid"
|
|
||||||
|
|
||||||
USE_SWAGGER = True
|
USE_SWAGGER = True
|
||||||
SESSION_CACHE_ALIAS = "session"
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
|
|
||||||
},
|
|
||||||
"session": {
|
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"LOCATION": values.Value(
|
"LOCATION": values.Value(
|
||||||
"redis://redis:6379/2",
|
"redis://redis:6379/2",
|
||||||
|
|||||||
Reference in New Issue
Block a user