Commit Graph
492 Commits
Author SHA1 Message Date
Manuel Raynaud 53eea07833 ✨(backend) wired soft deletion with yhub server
yhub is the source of truth, when a user delete a document, it should
also be deleted in the yhub server. We call the yhub server in the
perform_destroy action but also the restore endpoint of yhub when a
document is restored.
2026-09-22 16:00:54 +02:00
Manuel Raynaud 6bc0dd6f4a ✨(backend) add a migrate_documents command
command replaying the legacy content of
the documents into the collaboration server, one call to its migrate endpoint
per document. Resumable and safe to re-run: what became of every document is
recorded (`impress_document_migration`), a server that is unwell is retried
with a backoff and a document it refuses is left for a later run
(`--retry-failed`). Bounded by `--concurrency`, `--rate` and `--limit`, most
recently edited documents first
2026-09-22 16:00:54 +02:00
Manuel Raynaud c8b3c603c7 ✅(backend) correctly reload urls in tests
After removing most of the usage of S3 in the tests, these ones are
faster and make some flakyness more relevant. For example, in tests
related to the external api we have to reload the urls based on the
settings. We now have some race conditions where tests collapsed and
urls are not correctly reloaded.
2026-09-22 16:00:53 +02:00
Manuel Raynaud fe2c9cf83d ♻️(backend) remove usage of s3 for document.content in tests
Tehe DocumentFactory was always creating a content and this content was
saved on S3. This leads to the creation of huge amount of content in the
S3 storage but not necesseraly used in the tests. In order to keep the
refactor to remove the usage of content from document.content but from
Yhub service, this content is no more generated. It is kept for part of
the code not yet refactor like the versionning feature.
2026-09-22 16:00:53 +02:00
Manuel Raynaud 5a339a51c4 ♻️(backend) take adavantage of yhub 0.5.0 json encoding returns
The version 0.5.0 can manage response format by using accept and
content-type headers. In python we can't use for now the lib0 decoder so
we have to use the json format. When the lib0 decoder will be available
in pycrdt we will use it. So we can now use directly the /ydoc api to
fetch a document content instead the custom api made for this.
2026-09-22 16:00:52 +02:00
Manuel Raynaud f6bcbc0ee8 ♻️(backend) duplicate the onboarding sandbox using YHub service
Duplicate the onboarding sandbox document through the
collaboration server: its content is read from there and copied under the
identity of the user the sandbox is created for. A collaboration server that
cannot be reached skips the sandbox, as a missing template already did, and
never fails the signup
2026-09-22 16:00:52 +02:00
Manuel Raynaud e76a885fe5 ♻️(backend) index the document content from updated_content endpoint
the search indexer reads it with `YHubService`, and the indexation
of an edited document is triggered by the `content-updated` call
the collaboration server makes — nothing else sees the content change
anymore.
It is queued as a celery task, throttled like the other updates, so
no indexation ever runs in the process serving the request.
A document whose content cannot be read is left out of the batch
rather than indexed empty, which would have erased it from the search
backend
2026-09-22 16:00:51 +02:00
Manuel Raynaud cfe837bc32 ✨(collaboration) notify the backend when the worker persists new content
notify the backend when the worker persists new content for
a document, so the lists ordered by `updated_at` follow the edits made on the
collaboration server. The backend serves it on
`POST /api/v1.0/documents/{id}/content-updated/`, authenticated with a short
lived RS256 JWT the collaboration server signs (`aud: "docs-backend"`) and
the backend verifies against the JWKS the collaboration server publishes on
`/collaboration/jwks/v1` — the mirror of the admin token the backend signs to
call it, so no long lived secret is shared and either side can roll its key
on its own
2026-09-22 16:00:51 +02:00
Manuel Raynaud 3e10ba1b9f ✨(backend) serve documents/{id}/formatted-content/ from yhub
The formatted-content endpoint was using the `document.content` to fetch
the ydoc from s3, we want to move from this usage to using yhub to
retrieve the content, so yhub is becoming our source of thruth.
2026-09-22 16:00:51 +02:00
Manuel Raynaud 2408690380 💥(backend) remove the documents/{id}/content/ endpoint
Both its PATCH and its GET: the content of a document is saved and
served by the collaboration server. The `content_patch` and
`content_retrieve` abilities go with it.
2026-09-22 16:00:50 +02:00
Manuel Raynaud 565a8c0da6 ✨(backend) duplicate a document through the collaboration server
its stateis fetched from yhub and seeded into the copy instead
of being copied from the content stored by Django
2026-09-22 16:00:50 +02:00
Manuel Raynaud d200995c01 ✨(collaboration) add a get-ydoc endpoint on yhub
`GET /collaboration/get-ydoc/v1/docs/{id}` answers the current Yjs state of a
document as a raw binary update, the read counterpart of create-ydoc, and
204 when the document has no content yet
2026-09-22 16:00:50 +02:00
Manuel Raynaud 7e851c0133 ✨(backend) call YHubService to seed initial document content
When a new Docs is created and a file is sent, as before we convert it
first and we need to use the raw content to seed it by calling the
create-ydoc api in the YHub service.
2026-09-22 16:00:49 +02:00
Manuel Raynaud 95282ff410 ⏪️(backend) reintroduce the reset connection mechanism
When an access change or is deleted or a link configuration changes, we
call the yhub server to reset connections and remove them if needed. The
YHubService is used for this.
2026-09-22 16:00:49 +02:00
Manuel Raynaud 816849b72c ✨(backend) implement reset-connections and create-ydoc in YHubService
The reset-connections and create-ydoc are the first action we want to
implement in the YHubService. They will be used in next commits.
2026-09-22 16:00:49 +02:00
Manuel Raynaud 3e22da6949 ♻️(backend) audience is an enum to be used by the JWTService
To ease the use of the audience with the JWTService, we choose to create
an enum holding all the possible values and then use them in the Yhub
and Y-converter services.
2026-09-22 16:00:49 +02:00
Manuel Raynaud 6d0e2e6301 ✨(backend) add a service to call the yhub REST API
The backend application will have to call the yhub REST API for some
operations. We want to use a dedicated service to do that. This first
commit introduces the shape of this service, it only does the
configuration for now, calling actions will be implemented later.
2026-09-22 16:00:48 +02:00
Kevin JahnsandManuel Raynaud 1747f21b50 ✅(collaboration) test the legacy migrations against a real yhub
Cover both paths off the legacy Django store end to end: the lazy seed on
first access, and the migrate endpoint replaying every S3 version. The tests
need no database — the admin JWT short-circuits document authorization, so a
fixture is an S3 object on a random uuid — and read the timeline through
yhub 0.5.0's `Accept: application/json`, which spares python a lib0 decoder.
CI grows a valkey service and starts a collaboration server alongside the
backend test job; the tests skip themselves when nothing answers on the new
COLLABORATION_API_URL setting, so `make test` without the dev stack still
passes.

Writing them turned up three things worth fixing in the server.

Backend reads now seed too. getAccessType short-circuited on the admin token
before reaching the legacy store, so a server-side read of an unmigrated
document answered with an empty one, and a create-ydoc against it would have
written a second lineage beside the content the first user access was about
to seed in.

Seeding no longer decides access; the backend's answer alone does. A legacy
object that cannot be migrated — it does not decode, or it exceeds the size
we load — opens as a new document instead of denying, since no retry can fix
it and refusing would leave the document unopenable by anyone. The cause is
logged once per attempt with the bucket, key and stack, and every later access
logs that it admitted a caller without migrating.

That made the failure classifier dangerous, so it is inverted. It was an
allowlist of retryable errors — eight socket errnos — which left every way S3
can refuse (AccessDenied on a rotated key, NoSuchBucket, a region redirect)
counting as "this object is unusable". Denying, that was survivable; opening
empty, one misscoped credential would fork every document touched during the
window. Now only a failure raised while interpreting bytes we already hold is
permanent, marked at the throw site, and everything else answers a retryable
503. Guessing wrong that way costs a retry; the other way costs the document.

The admin seed is also fenced to the org and to main, like the user path
above it. The legacy store is branchless — {docid}/file is main — and the
bookkeeping is per document, so seeding ?branch=draft would have written
main's content into an orphan room and left the real one permanently empty.

Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-22 16:00:47 +02:00
Anthony LCandManuel Raynaud 3184dd184c 🛂(backend) add audience to jwt
Add audience to the jwt, scoping the token to it
prevents an admin JWT issued for another backend
service from being replayed against y-provider.
2026-09-22 16:00:44 +02:00
Anthony LCandManuel Raynaud 784dadbaca 🛂(django) use jwt token for converter services
The Y_PROVIDER_API_KEY shared secret is replaced by a
signed admin JWT when Django calls the y-provider
conversion endpoint.
2026-09-22 16:00:43 +02:00
Manuel Raynaud ebfc759053 🔥(backend) remove CollaborationService and can-edit endpoint
The CollaborationService was doing nothing since we started the
migration to yhub, all the code using it is now removed. Also the
`can-edit` endpoint and all the safeguard mechanism relying on the
presence of other users connected to the websocket will not be used
anymore, it will be possible to replace all of this with yhub, so all
this code is also removed.
2026-09-22 16:00:42 +02:00
Manuel Raynaud 61b396ebc6 ✨(backend) add a method to create a dedicated admin token
For now the only token we will need is ont with the admin claim set to
True. To not repeat the creation of this token again and again, we
created a dedicated method to issue this token in the JWTService class.
2026-09-22 16:00:41 +02:00
Manuel Raynaud af0cdeaf1a ✨(backend) publish the JWT public key on a JWKS endpoint
The yhub service will need our public key in order to validate the jwt
token we will used. We choose to expose a jwks endpoint as it is a
standard wat to do this.
2026-09-22 16:00:41 +02:00
Manuel Raynaud b9cd646605 ✨(backend) add a service generating cached RS256 JWT tokens
We want to generate jwt token using the RS256 algotrithm. This token
will be used for internal call with the yhub service.
2026-09-22 16:00:39 +02:00
Kevin JahnsandManuel Raynaud eb94d726b0 ♻️(collaboration) switch collaboration server from hocuspocus to yhub
Signed-off-by: Kevin Jahns <kevin.jahns@protonmail.com>
2026-09-22 16:00:22 +02:00
Anthony LC d01372fd90 ♻️(backend) return the full document in the duplicate response
The duplicate endpoint used to respond with only `{"id": ...}`. It now
returns the complete duplicated document representation, consistent
with the other document detail endpoints, so the frontend doesn't have
to make a follow-up request to get the new document's data.

This required setting `is_favorite` explicitly on the duplicated
document before serializing it: it is normally set by the
`annotate_is_favorite` queryset method, which the newly created
document never goes through. Being a read-only serializer field, it
was silently dropped from the response instead of raising an error. A
document can't be a favorite right after being created, so it is set
to `False` directly.
2026-09-18 16:17:31 +02:00
risk-altandAnthony LC 5b661d7224 🥅(frontend) warn before uploading a file over the size limit
Dropping a file larger than the allowed size showed a bare "unknown
error" in the editor. The proxy in front of the API cuts the request
and answers a 413 with an HTML body, so errorCauses threw while
parsing it as JSON and no cause ever reached the error panel.

The size limit the backend already enforces is now exposed by the
config endpoint, and the editor checks the file against it before
sending anything, with the same toast wording the document import
uses. errorCauses no longer throws on a body it cannot parse, and a
413 without a usable cause falls back to an explicit message, which
covers the instances whose proxy limit is lower than the application
one.

The size formatting duplicated in the import hook moved to a shared
util.

Signed-off-by: risk-alt <aldu6974@gmail.com>
2026-09-17 16:04:26 +02:00
Julien MaupetitandGitHub 31cff890b8 🚚(global) move favorite documents API endpoint to /documents/favorites/
To respect the globally used pattern, we can safely switch to a simpler
path
2026-09-14 10:01:36 +00:00
Manuel Raynaud 451499016e ⚡️(backend) increase nb_accesses cache TTL
The nb_accesses cache TTL was very short, 30 seconds. That mean that the
user will hit the cache for a very short period and the cache is
probably not be hit. This is what we can see in the slow queries from
the pg_stat_statements table. The query to compute the nb_accesses is
executed a little bit less than the number of queries to list or
retrieve documents, meaning the cache is not used.
2026-09-11 12:55:20 +02:00
Manuel Raynaud 6baf20aaeb ⚡️(backend) improve DocumentViewset.get_queryset
The filtering made in the DocumentViewset.get_queryset method is not
optimal and lead to a full scan of the Document table. The heavy part is
on the filtering on what the user can access between the accesses and
the link traces. To have better performance we make an union operation
of both document_id list and the filter the id on this list. Postgresql
will use the index on the id column.
2026-09-11 12:55:20 +02:00
Manuel Raynaud fbc3ef83ba ⚡️(backend) stop using LEFT(value, LENGTH(path)) in sql queries
Comparing path with LEFT(value, LENGTH(path)) makes a sequential scan on
all the Document table, the more this table grow, the more the query
using it will be slow. We dediced instead to lookup on the path
extracting all ancestors path for a given document and then make a path
IN statement to use the index existing on the path column.
2026-09-11 12:55:20 +02:00
Manuel Raynaud 0b8808f9a7 🐛(backend) skip session creation for the readiness probe
The readiness probe should also not create a new session. A new session
will live in redis and increase the number of keys inside it for
nothing. The readiness path is isgnored in the ForceSessionMiddleware
2026-09-08 14:44:57 +02:00
Manuel Raynaud 1e61b4a789 🐛(backend) skip session creation for the liveness probe
The ForceSessionMiddleware force the session creation, we want to
ignore it when the request is the liveness probe. The liveness probe
must not check if redis is available, this is the readiness probe job
2026-09-08 08:56:42 +02:00
Manuel Raynaud 53bf783447 🐛(backend) manage async support for Docs custom middleware
Docs have 2 custom middlewares, both are only managing sync
requests. With Python 3.13 we didn't have any errors, but
since we upgraded to Python 3.14, we have a CancelledError
exception. We decided to use the MiddlewareMixin from Django
that is sync and async capable and will be responsible for
executing both middleware in the good mode.
2026-09-03 08:25:03 +02:00
Amine BOUKERFAandGitHub 681f9a8c40 🐛(backend) fix duplicating a document that has no conten
Document.content reads from object storage and returns None when nothing
was ever written there. That None, raised "content should be a string.",
so the duplicate endpoint answered a 500. Default to an empty string instead.
    
Signed-off-by: BOUKERFA Mohamed El Amine <boukerfa.ma@gmail.com>
2026-09-02 06:46:37 +00:00
Manuel Raynaud 4111e4e5ed ⚡️(backend) optimize media_auth cpu usage
Once the sql queries improved we have still a bottleneck on large
concurrent requests on this endpoint. We notive in the profiles generated
that lot of time was spent in creating a new s3 client instance on each
request. django_storage use a thread local cache for signed and unsigned
connection, but using uvicorn we have a new thread for each request, so
on each request a new s3 client is generated and it appears to be an
expensive operation. To fix this issue, we cache the client and share it
accross all the thread and requests.
2026-08-20 17:28:31 +02:00
Manuel Raynaud 7372c4610f ⚡️(backend) optimize media_auth sql queries
On the media_auth endpoint the first bottleneck we have is with
postgresql. We are looking for too much data and no index is used on the
attachments colum. When the lookup filter on the attachement columns, a
full scan is made on all the document table looking for each element in
the array, this operation is really expensive. To fix this we created a
GIN index on the attachments column. Also the readable_per_se lookup was
selecting too much data combined with the filter_descendants function.
We remove the usage of the filter_descendants, we choose to first fetch
all the paths where the attachment is found, this operation is fast
thanks to the new index, split all the paths in candidate paths and then
filter readable_per_se queryset with these paths. All these
modifications make the endpoint faster.
2026-08-20 16:28:31 +02:00
Manuel Raynaud f714c7fae3 ✨(backend) profile api using django-silk
We have performance issue with the media_auth endpoint. We need to
monitor and profile this endpoint to understand what is happening. We
decided to install and configure django-silk. By default django-silk is
not enabled and not accessible, once enabled migration must be run and
the /silk/ endpoint will be accessible only using a super user.
To have relecant profiles and monitor we must have a database comparable
to a production one because we are facing a scaling issue. Unless a
million documents probably nothing is happening, for this we added
several management commands to help us. A first one to anonymize a
database than can be reused without any risk to leak PII data, can be
called using python manage.py anonymize_database with some mandatory
options. An other command to generate a database profile, this command
should be run on a production database, this profile can then be used by
the generate_volumetry to populate a development database with same
volumetry than describe in the profile.
2026-08-20 16:21:36 +02:00
risk-altandAnthony LC fb984abab3 🐛(backend) handle object storage metadata keys case-insensitively
Object storage metadata keys are case insensitive per the S3
specification, but implementations don't agree on the case they give
back. When head_object returns a capitalized "Status", updating the
attachment status added a second, lowercase entry instead of replacing
it, and the copy request ended up carrying two x-amz-meta-status
headers. Ceph RadosGW loses one of them behind a proxy, which
invalidates the request signature.

The same assumption was made when reading the status back in media-auth
and media-check, where an attachment stored on such a backend stayed in
"processing" forever.

Metadata read from the storage is now normalized to lowercase keys
before being consumed or copied over.

Signed-off-by: risk-alt <aldu6974@gmail.com>
2026-08-14 11:30:52 +02:00
Manuel Raynaud f7d06e45e5 🚨(backend) lint code with new ruff 0.16
New ruff version need to lint the code again and adapt exceptions
2026-08-04 10:16:51 +02:00
Manuel Raynaud a57fb7cbde ♻️(backend) remove deprecation warning from pydantic-ai library
Since pydantic-ai has ben upgraded deprecation warning were present in
the logs. This commint change how the instrumentation is enabled to be
compatible with hoe pydantic-ai manage it. Also prompt system are
ignored when they come from the front, like we are doing and dropped. To
maintain the system prompt we use the instruction parameter instead.
2026-08-04 10:16:51 +02:00
Manuel Raynaud 050591e2ee ⬆️(backend) upgrade pydantic-ai-slim and remove mistralai
We upgraded pydantic-ai-slim to the latest version and add mistral has
extra dependency. Doing this, the direct dependance to mistral can be
removed and will be managed with pidantic-ai-slim package. The import of
mistralai has changed, we have to updagre them from mistralai to
mistralai.client
2026-08-04 10:16:50 +02:00
Florent Chehab edf26d6321 ✨(backend) conditional email notification in server to server api
Add send_notification_email flag (defaults to True) on
the ServerCreateDocumentSerializer so that we can bypass
sending the email notification when creating a document
with the server to server API.

Signed-off-by: Florent Chehab <flo@nextmile.fr>
2026-08-03 10:54:31 +02:00
Manuel Raynaud d35b81a6ed ♻️(backend) reset collaboration connection in cascade for all children
When an access is updated or deleted, or a link_configuration is
changed, the collaration_service is used to reset the connection in the
collaboration server. As accesses and link_configuration are inherited
in a Docs tree, if the user is connected to a child, the connection is
not reset. This commit fix this issue by calling the reset on every
children in the tree.
2026-07-09 14:46:11 +02:00
Manuel Raynaud 9ef4a15f5e ♻️(backend) scope document search by document id instead of path
The search in a document tree was triggered by the usage of the document
path. The path is something guessable by incrementing it you can
discover public documents. We decided to change this to use the document
id which is not guessable and prevent discovering public documents.
Thanks to @maboukerfa for discovering it.
2026-07-07 11:21:47 +02:00
Manuel Raynaud b01e6d58fd ♻️(core) fix typo in settings COLLABORATION_WS_NOT_CONNECTED_READY_ONLY
The settings COLLABORATION_WS_NOT_CONNECTED_READY_ONLY contains a typo.
We don't want to see READY_ONLY but READ_ONLY. This commit fix the typo
and change everywhere in the codebase the settings name.
2026-07-06 08:55:21 +02:00
Manuel RaynaudandGitHub debb253fa5 ✨(backend) add management command to reset a Document
We need a management command to reset a Document to an initial state and
deletes everything related to it. This command can be usefull to reset a
demo for example.
2026-07-03 15:10:04 +00:00
Manuel RaynaudandGitHub 336d530014 🛂(backend) remove unused default authentication backend
The authentication backend
mozilla_django_oidc.contrib.drf.OIDCAuthentication
is present in the default authentication classes for the REST_FRAMEWORK
settings. This backend should not be used by our application and can
lead to the usage of our main api with an access_token instead of the
cookie session.
We need to override the drf SessionAuthentication backend to implement
the authenticate_header method. Without this, a 403 status code is
returned, but it is not valid. It must a be 401
2026-07-03 13:18:51 +00:00
Dakshesh JainandGitHub 6ec541f5cb 🐛(backend) update restore ability for inherited deletion
Updated the restore ability so that only directly deleted documents can
be restored. This prevents the restore action from being exposed for
child documents whose parent is deleted. This PR addresses #2127.
2026-06-30 10:01:07 +00:00
Amine BOUKERFAandGitHub 36dced1e56 🐛(backend) prevent owner from leaving a soft-deleted document
Owners could leave trashed documents via the leave endpoint, deleting
their `DocumentAccess` and all subtree access. When the last owner
leaves a document it creates an orphan as no owners remains in the doc.

Signed-off-by: BOUKERFA Mohamed El Amine <boukerfa.ma@gmail.com>
2026-06-29 13:04:04 +00:00