diff --git a/backend/letters/serializers.py b/backend/letters/serializers.py index 7a4c9eb..455c805 100644 --- a/backend/letters/serializers.py +++ b/backend/letters/serializers.py @@ -1,3 +1,5 @@ +from datetime import UTC, datetime, timedelta + from rest_framework import serializers from letters.models import Letter, LetterImage @@ -34,6 +36,16 @@ class LetterSerializer(serializers.ModelSerializer): ] read_only_fields = ["created_at", "updated_at"] + def to_representation(self, instance): + fields = super().to_representation(instance) + if fields["type"] == Letter.Type.VAULT and fields["status"] == Letter.Status.SEALED: + unlock_datetime = datetime.fromisoformat(fields["unlock_at"]).replace(tzinfo=UTC) + if unlock_datetime - datetime.now(tz=UTC) > timedelta(seconds=0): + fields["encrypted_content"] = None + fields["images"] = None + fields["encrypted_dek"] = None + return fields + def validate(self, data): """ Validates the requirmnt of DEK when encrypted content and metadata are stored. diff --git a/backend/letters/tests.py b/backend/letters/tests.py index ac8b886..2be80fc 100644 --- a/backend/letters/tests.py +++ b/backend/letters/tests.py @@ -1,3 +1,5 @@ +from datetime import UTC + from django.contrib.auth import get_user_model from django.core.files.base import ContentFile from django.test import TestCase @@ -208,6 +210,42 @@ class LetterAPITest(APITestCase): self.assertFalse(default_storage.exists("encrypted-images/old2.bin")) self.assertEqual(response.status_code, 200) + def test_vault_letters_does_not_return_letter_content_before_the_unlock_date(self): + """ + Test that the vault letters does not return letter content (images and encrypted_content) + before the unlock date. + """ + from datetime import datetime, timedelta + + letter = Letter.objects.create( + user=self.user, + type="VAULT", + status="SEALED", + public_id="4281edcc-5459-4ff2-bb5e-669fb44e0757", + encrypted_content="enc_content==", + encrypted_metadata="enc_meta==", + encrypted_dek="enc_dek==", + unlock_at=datetime.now(UTC), + ) + from freezegun import freeze_time + + past_datetime = datetime.now(UTC) - timedelta(seconds=1) + future_datetime = datetime.now(UTC) + timedelta(seconds=1) + + with freeze_time(past_datetime): + response = self.client.get(f"/api/letters/{letter.public_id}/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["encrypted_content"], None) + self.assertEqual(response.data["encrypted_metadata"], "enc_meta==") + self.assertEqual(response.data["encrypted_dek"], None) + + with freeze_time(future_datetime): + response = self.client.get(f"/api/letters/{letter.public_id}/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["encrypted_content"], "enc_content==") + self.assertEqual(response.data["encrypted_metadata"], "enc_meta==") + self.assertEqual(response.data["encrypted_dek"], "enc_dek==") + class LetterImageModelTest(TestCase): def setUp(self): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1189ccb..7e39043 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "django-extensions>=4.1", "djangorestframework>=3.17.1", "djangorestframework-simplejwt>=5.5.1", + "freezegun>=1.5.5", "psycopg2-binary>=2.9.11", "pyopenssl>=26.0.0", "ruff>=0.15.9", diff --git a/backend/uv.lock b/backend/uv.lock index 956c272..96f4680 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -171,6 +171,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, ] +[[package]] +name = "freezegun" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -212,6 +224,7 @@ dependencies = [ { name = "django-extensions" }, { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, + { name = "freezegun" }, { name = "psycopg2-binary" }, { name = "pyopenssl" }, { name = "ruff" }, @@ -226,6 +239,7 @@ requires-dist = [ { name = "django-extensions", specifier = ">=4.1" }, { name = "djangorestframework", specifier = ">=3.17.1" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, + { name = "freezegun", specifier = ">=1.5.5" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "pyopenssl", specifier = ">=26.0.0" }, { name = "ruff", specifier = ">=0.15.9" }, @@ -281,6 +295,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "ruff" version = "0.15.9" @@ -306,6 +332,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sqlparse" version = "0.5.5"