diff --git a/.env.e2e.example b/.env.e2e.example index 62e776f..9c1f97b 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -1,25 +1,27 @@ -# Pi Ku E2E Environment Configuration Template +# DATABASE +DB_NAME=piku_test_db +DB_USER=test +DB_PASSWORD=password123 +DB_HOST=localhost +DB_PORT=5433 -# Database (Postgres) -E2E_DB_NAME=piku_e2e_db -E2E_DB_PORT=5433 -E2E_DB_USER=piku_test -E2E_DB_PASS=piku_test -E2E_DB_DB=piku_e2e +# SSL +SSL_ENABLED=false -# Backend (Django) -E2E_BACKEND_PORT=8001 -SECRET_KEY=e2e-secret-key-for-piku-testing +# DJANGO DEBUG=True -ALLOWED_HOSTS=* -CORS_ALLOWED_ORIGINS=http://localhost:5173 -FRONTEND_URL=http://localhost:5173 -EMAIL_HOST=localhost -EMAIL_PORT=1025 +SECRET_KEY=django-insecure-initial-key +BACKEND_DOMAIN=127.0.0.1 +BACKEND_PORT=8001 + +# EMAIL +EMAIL_HOST=127.0.0.1 +EMAIL_PORT=1026 +FROM_EMAIL="Test " EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= -FROM_EMAIL=testing@piku.local -MAILPIT_API_URL=http://localhost:8025/api/v1 +EMAIL_API_PORT=8026 -# Frontend (Vite/Playwright) -VITE_API_URL=http://localhost:8001 +# FRONTEND +FRONTEND_PORT=5199 +FRONTEND_DOMAIN=127.0.0.1 diff --git a/.env.example b/.env.example index b9455ed..5554422 100644 --- a/.env.example +++ b/.env.example @@ -5,21 +5,20 @@ DB_PASSWORD=password123 DB_HOST=localhost DB_PORT=5432 +# SSL +SSL_ENABLED=true + # DJANGO DEBUG=True SECRET_KEY=django-secret-key -ALLOWED_HOSTS=localhost,127.0.0.1 -CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +BACKEND_DOMAIN=127.0.0.1 +BACKEND_PORT=8000 # EMAIL -EMAIL_HOST=localhost +EMAIL_HOST=127.0.0.1 EMAIL_PORT=1025 -FROM_EMAIL=Pi Ku -EMAIL_HOST_USER=root -EMAIL_HOST_PASSWORD=password123 +FROM_EMAIL=Pi Ku # FRONTEND -VITE_API_URL=http://localhost:8000 FRONTEND_PORT=5173 -FRONTEND_URL=http://localhost:5173 -FRONTEND_DOMAIN=localhost +FRONTEND_DOMAIN=127.0.0.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04ff0a6..8c60305 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,34 +7,59 @@ on: branches: [ main ] jobs: + setup-environment: + name: Generate Certificates + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Generate SSL Certificates + run: | + sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools + mkdir -p certs + mkcert -install + mkcert -cert-file certs/localhost.pem -key-file certs/localhost-key.pem localhost 127.0.0.1 ::1 + + - name: Cache certificates + uses: actions/cache/save@v4 + with: + path: certs + key: certs-${{ runner.os }}-${{ github.sha }} + frontend: name: Frontend CI runs-on: ubuntu-latest + needs: setup-environment defaults: run: working-directory: ./frontend steps: - uses: actions/checkout@v4 - - name: Create .env from example - run: cp ../.env.example ../.env - uses: oven-sh/setup-bun@v2 + + - name: Restore certificates + uses: actions/cache/restore@v4 with: - bun-version: latest + path: certs + key: certs-${{ runner.os }}-${{ github.sha }} + - name: Install dependencies run: bun install --frozen-lockfile - - name: Code Quality (Biome) - run: bun run check + + - name: Code Quality + run: | + cp ../.env.example ../.env + bun run check + - name: Type Check & Build run: bun run build - - name: Run Unit Tests + + - name: Unit Tests run: bun run test backend: name: Backend CI runs-on: ubuntu-latest - defaults: - run: - working-directory: ./backend + needs: setup-environment services: postgres: image: postgres:16-alpine @@ -44,63 +69,79 @@ jobs: POSTGRES_PASSWORD: password123 ports: - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 # wait till up before starting (integrating) django app + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + defaults: + run: + working-directory: ./backend steps: - uses: actions/checkout@v4 - - name: Create .env from example - run: cp ../.env.example ../.env - name: Install uv uses: astral-sh/setup-uv@v5 with: - version: "latest" enable-cache: true - cache-dependency-glob: backend/uv.lock - - name: Install dependencies - run: uv sync - - name: Lint (Ruff) - run: uv run ruff check - - name: Run Tests - run: uv run python manage.py test + cache-dependency-glob: "backend/uv.lock" + + - name: Restore certificates + uses: actions/cache/restore@v4 + with: + path: certs + key: certs-${{ runner.os }}-${{ github.sha }} + + - name: Setup Environment + run: | + cp ../.env.example ../.env + uv sync + + - name: Lint & Test + run: | + uv run ruff check + uv run python manage.py test e2e: name: E2E Tests runs-on: ubuntu-latest - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_DB: piku_e2e - POSTGRES_USER: piku_test - POSTGRES_PASSWORD: piku_test - ports: - - 5433:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 - mailpit: - image: axllent/mailpit:latest - ports: - - 8025:8025 - - 1025:1025 + needs: setup-environment steps: - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 + + - name: Restore Certificates + uses: actions/cache/restore@v4 with: - version: "latest" + path: certs + key: certs-${{ runner.os }}-${{ github.sha }} + + - name: Setup Tools + uses: astral-sh/setup-uv@v5 + - uses: oven-sh/setup-bun@v2 - - name: Install Frontend dependencies - run: cd frontend && bun install - - name: Install Playwright Browsers - run: cd frontend && bun x playwright install --with-deps - - name: Create .env.e2e - run: cp .env.e2e.example .env.e2e - - name: Run E2E Script - run: ./scripts/run-e2e.sh + + + - name: Cache Playwright + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('frontend/bun.lock') }} + + - name: Install Dependencies + run: | + (cd frontend && bun install) + if [ "${{ steps.playwright-cache.outputs.cache-hit }}" != "true" ]; then + (cd frontend && bun x playwright install --with-deps) + fi + + - name: Run E2E + run: | + cp .env.e2e.example .env.e2e + chmod +x ./scripts/run-e2e.sh + ./scripts/run-e2e.sh env: CI: "true" + - name: Upload Playwright Report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: frontend/playwright-report/ - retention-days: 30 + retention-days: 10 diff --git a/.gitignore b/.gitignore index f10ac95..97471a6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ dist/ # IDE .vscode/ + +# Certificates +certs/*.pem +tmp/ diff --git a/backend/config/settings.py b/backend/config/settings.py index 300ddd6..bf70160 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -19,12 +19,17 @@ import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Load environment variables +# Load dotenv files env = environ.Env() -# Allow overriding the .env file path (useful for E2E testing/CI) -env_file = os.environ.get("PIKU_ENV_FILE", os.path.join(BASE_DIR.parent, ".env")) +env_file = os.path.join(BASE_DIR.parent, ".env") if os.path.exists(env_file): - environ.Env.read_env(env_file) + environ.Env.read_env(env_file, overwrite=False) + + +SSL_ENABLED = env("SSL_ENABLED") == "true" +FRONTEND_URL = f"https://{env('FRONTEND_DOMAIN')}" if SSL_ENABLED else f"http://{env('FRONTEND_DOMAIN')}" +if env("FRONTEND_PORT"): + FRONTEND_URL += f":{env('FRONTEND_PORT')}" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ @@ -35,7 +40,7 @@ SECRET_KEY = env("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") -ALLOWED_HOSTS = env.list("ALLOWED_HOSTS") or [] +ALLOWED_HOSTS = [env("FRONTEND_DOMAIN")] # Application definition @@ -45,10 +50,12 @@ INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.staticfiles", + "django_extensions", "rest_framework", "corsheaders", "users", "letters", + "scripts", ] MIDDLEWARE = [ @@ -81,7 +88,7 @@ DATABASES = { } } -CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS") +CORS_ALLOWED_ORIGINS = [FRONTEND_URL] CORS_ALLOW_CREDENTIALS = True AUTH_USER_MODEL = "users.User" @@ -106,7 +113,7 @@ NOTE: COOKIE_SAMESITE: Lax is used to allow cross-site redirection, like links AUTH_COOKIE = { "NAME": "refresh_token", "DOMAIN": None, - "SECURE": not DEBUG, + "SECURE": SSL_ENABLED, "HTTPONLY": True, "SAMESITE": "Lax", } @@ -116,12 +123,8 @@ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = env("EMAIL_HOST") EMAIL_PORT = env("EMAIL_PORT") EMAIL_USE_TLS = not DEBUG -EMAIL_HOST_USER = env("EMAIL_HOST_USER") -EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") FROM_EMAIL = env("FROM_EMAIL") -FRONTEND_URL = env("FRONTEND_URL") - # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators diff --git a/backend/pyproject.toml b/backend/pyproject.toml index b651a16..1189ccb 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,17 +1,20 @@ [project] -name = "backend" +name = "piku_backend" version = "0.1.0" -description = "Add your description here" +description = "Django Rest Framework for handling requests for Pi Ku app" readme = "README.md" requires-python = ">=3.14" dependencies = [ "django>=6.0.4", "django-cors-headers>=4.9.0", "django-environ>=0.13.0", + "django-extensions>=4.1", "djangorestframework>=3.17.1", "djangorestframework-simplejwt>=5.5.1", "psycopg2-binary>=2.9.11", + "pyopenssl>=26.0.0", "ruff>=0.15.9", + "werkzeug>=3.1.8", ] @@ -23,4 +26,4 @@ line-length = 120 select = ["E", "F", "W", "UP", "I"] [tool.ruff.lint.per-file-ignores] -"**/migrations/*" = ["E501"] # boilerplate - ignore +"**/migrations/*" = ["E501"] diff --git a/backend/scripts/__init__.py b/backend/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/scripts/management/commands/__init__.py b/backend/scripts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/scripts/management/commands/serve.py b/backend/scripts/management/commands/serve.py new file mode 100644 index 0000000..9da8067 --- /dev/null +++ b/backend/scripts/management/commands/serve.py @@ -0,0 +1,30 @@ +import os + +from django.conf import settings +from django.core.management import call_command +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **options): + """ + Check if SSL is enabled in the environment variables. + If SSL is enabled, use runserver_plus command. + If SSL is not enabled, use runserver command. + """ + ssl_enabled = os.getenv("SSL_ENABLED", "false").lower() == "true" + domain = os.getenv("BACKEND_DOMAIN", "127.0.0.1") + port = os.getenv("BACKEND_PORT", "8000") + addrport = f"{domain}:{port}" + + if ssl_enabled: + self.stdout.write(self.style.SUCCESS(f"Starting with SSL on {addrport}...")) + call_command( + "runserver_plus", + addrport, + cert_file=settings.BASE_DIR / "../certs/localhost.pem", + key_file=settings.BASE_DIR / "../certs/localhost-key.pem", + ) + else: + self.stdout.write(self.style.WARNING(f"Starting without SSL on {addrport}...")) + call_command("runserver", addrport) diff --git a/backend/users/tests.py b/backend/users/tests.py index 6bea060..cff262b 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,3 +1,5 @@ +from unittest.mock import _patch_dict + from django.contrib.auth import get_user_model from django.contrib.auth.tokens import default_token_generator from django.urls import reverse @@ -19,9 +21,10 @@ class AuthTests(APITestCase): self.refresh_url = reverse("token_refresh") self.logout_url = reverse("logout") + @_patch_dict("config.settings.AUTH_COOKIE", {"SECURE": True}) def test_login_sets_secure_cookie(self): """ - Tests if the Login API can generate access token and set secure cookie for refresh token. + Tests if the Login API can generate access token and set secure cookie (when ssl is enabled) for refresh token. """ data = {"email": self.user.email, "password": self.password} cookie_name = "refresh_token" @@ -32,9 +35,10 @@ class AuthTests(APITestCase): self.assertIn("access", response.data) self.assertNotIn("refresh", response.data) self.assertIn(cookie_name, response.cookies) - self.assertTrue(response.cookies[cookie_name].value) - self.assertTrue(response.cookies[cookie_name].httponly) - self.assertEqual(response.cookies[cookie_name]["samesite"], "Lax") + self.assertIsNotNone(response.cookies[cookie_name].value) + self.assertTrue(response.cookies[cookie_name].get("httponly")) + self.assertTrue(response.cookies[cookie_name].get("secure")) + self.assertEqual(response.cookies[cookie_name].get("samesite"), "Lax") class ActivationTests(APITestCase): diff --git a/backend/uv.lock b/backend/uv.lock index 89f1e42..956c272 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -12,28 +12,89 @@ wheels = [ ] [[package]] -name = "backend" -version = "0.1.0" -source = { virtual = "." } +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "django-cors-headers" }, - { name = "django-environ" }, - { name = "djangorestframework" }, - { name = "djangorestframework-simplejwt" }, - { name = "psycopg2-binary" }, - { name = "ruff" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[package.metadata] -requires-dist = [ - { name = "django", specifier = ">=6.0.4" }, - { name = "django-cors-headers", specifier = ">=4.9.0" }, - { name = "django-environ", specifier = ">=0.13.0" }, - { name = "djangorestframework", specifier = ">=3.17.1" }, - { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, - { name = "psycopg2-binary", specifier = ">=2.9.11" }, - { name = "ruff", specifier = ">=0.15.9" }, +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -72,6 +133,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e", size = 20682, upload-time = "2026-02-18T01:08:07.359Z" }, ] +[[package]] +name = "django-extensions" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" }, +] + [[package]] name = "djangorestframework" version = "3.17.1" @@ -98,6 +171,67 @@ 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 = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "piku-backend" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "django-cors-headers" }, + { name = "django-environ" }, + { name = "django-extensions" }, + { name = "djangorestframework" }, + { name = "djangorestframework-simplejwt" }, + { name = "psycopg2-binary" }, + { name = "pyopenssl" }, + { name = "ruff" }, + { name = "werkzeug" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=6.0.4" }, + { name = "django-cors-headers", specifier = ">=4.9.0" }, + { name = "django-environ", specifier = ">=0.13.0" }, + { name = "django-extensions", specifier = ">=4.1" }, + { name = "djangorestframework", specifier = ">=3.17.1" }, + { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "pyopenssl", specifier = ">=26.0.0" }, + { name = "ruff", specifier = ">=0.15.9" }, + { name = "werkzeug", specifier = ">=3.1.8" }, +] + [[package]] name = "psycopg2-binary" version = "2.9.11" @@ -117,6 +251,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -126,6 +269,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] +[[package]] +name = "pyopenssl" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } +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 = "ruff" version = "0.15.9" @@ -168,3 +323,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40 wheels = [ { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, ] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] diff --git a/biome.json b/biome.json index 1dca93a..07c7ff8 100644 --- a/biome.json +++ b/biome.json @@ -42,7 +42,7 @@ "noUnusedVariables": "error" } }, - "includes": ["**", "!backend"] + "includes": ["**/src", "!backend"] }, "assist": { "actions": { diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 0000000..f94990b --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,19 @@ +services: + db: + image: postgres:16-alpine + container_name: ${DB_NAME} + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + ports: + - "${DB_PORT}:5432" + restart: unless-stopped + + mailpit: + image: axllent/mailpit + container_name: piku_test_mail + ports: + - "${EMAIL_API_PORT}:8025" + - "${EMAIL_PORT}:1025" + restart: unless-stopped diff --git a/frontend/bun.lock b/frontend/bun.lock index e1e1378..d6c45c6 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -34,6 +34,7 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.3.0", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", "dotenv": "^17.4.2", @@ -257,6 +258,8 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@2.3.0", "", { "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.4", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.4", "vitest": "4.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w=="], diff --git a/frontend/e2e/utils/auth.ts b/frontend/e2e/utils/auth.ts index f056f4d..9210011 100644 --- a/frontend/e2e/utils/auth.ts +++ b/frontend/e2e/utils/auth.ts @@ -34,6 +34,7 @@ export async function registerAndLogin( // 2. Activation via Mailpit logger.info(`[Auth] Polling Mailpit for activation email...`); const activationLink = await MailpitHelper.getActivationLink(email); + await page.goto(activationLink); await expect(page.getByText(/account activated/i)).toBeVisible(); diff --git a/frontend/e2e/utils/mailpit.ts b/frontend/e2e/utils/mailpit.ts index f16df52..9f78b35 100644 --- a/frontend/e2e/utils/mailpit.ts +++ b/frontend/e2e/utils/mailpit.ts @@ -7,7 +7,7 @@ export interface MailpitMessage { To: { Address: string }[]; } -const MAILPIT_API_URL = process.env.MAILPIT_API_URL; +const MAILPIT_API_URL = `http://${process.env.EMAIL_HOST}:${process.env.EMAIL_API_PORT}/api/v1`; export const MailpitHelper = { getActivationLink: async ( @@ -18,7 +18,6 @@ export const MailpitHelper = { const requestContext = await request.newContext(); while (Date.now() - startTime < timeout) { - // Search specifically for the recipient to reduce data transfer const response = await requestContext.get(`${MAILPIT_API_URL}/search`, { params: { query: `to:${email}`, limit: 1 }, }); diff --git a/frontend/package.json b/frontend/package.json index ef22a80..4478276 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,7 @@ "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.3.0", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", "dotenv": "^17.4.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index fc66cf2..95c106c 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,12 +1,20 @@ import path from "node:path"; -import process from "node:process"; +import process, { env } from "node:process"; import { defineConfig, devices } from "@playwright/test"; import dotenv from "dotenv"; +import { getBaseUrl } from "./utils/url-builder"; /** * Read environment variables from file. */ dotenv.config({ path: path.resolve(process.cwd(), "../.env.e2e") }); +const baseUrl = getBaseUrl( + env.SSL_ENABLED === "true", + env.FRONTEND_DOMAIN, + env.FRONTEND_PORT, +); + +console.log(baseUrl); export default defineConfig({ timeout: 60000, expect: { @@ -26,13 +34,17 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.FRONTEND_URL, + baseURL: baseUrl, /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 20000, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", /* Capture screenshot on failure */ screenshot: "only-on-failure", + /* Capture video on failure */ + video: "retain-on-failure", + /* Ignore HTTPS errors */ + ignoreHTTPSErrors: true, }, /* Configure projects for major browsers */ @@ -49,8 +61,13 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "bun run dev", - url: process.env.FRONTEND_URL, + command: "bun run dev -- --mode e2e", + url: getBaseUrl( + process.env.SSL_ENABLED === "true", + process.env.FRONTEND_DOMAIN, + process.env.FRONTEND_PORT, + ), reuseExistingServer: !process.env.CI, + ignoreHTTPSErrors: true, }, }); diff --git a/frontend/src/api/apiClient.test.ts b/frontend/src/api/apiClient.test.ts index 44a439f..074bc8e 100644 --- a/frontend/src/api/apiClient.test.ts +++ b/frontend/src/api/apiClient.test.ts @@ -13,7 +13,7 @@ import { server } from "../../test/mocks/server"; import { useAuthStore } from "../store/useAuthStore"; import { api } from "./apiClient"; -const API_URL = "http://piku-server"; +const VITE_API_URL = "http://piku-server"; beforeEach(() => { useAuthStore.setState({ @@ -24,7 +24,7 @@ beforeEach(() => { }); beforeAll(() => { - vi.stubEnv("API_URL", API_URL); + vi.stubEnv("VITE_API_URL", VITE_API_URL); }); afterAll(() => { @@ -37,7 +37,7 @@ describe("request interceptor", () => { let capturedAuthHeader = ""; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { capturedAuthHeader = request.headers.get("Authorization") ?? ""; return HttpResponse.json(mockUser); }), @@ -51,7 +51,7 @@ describe("request interceptor", () => { it("should not send Authorization header when the store has no token", async () => { let capturedAuthHeader: string | null = ""; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { capturedAuthHeader = request.headers.get("Authorization"); return HttpResponse.json({}); }), @@ -70,14 +70,14 @@ describe("response interceptor", () => { let _refreshApiCallCount = 0; server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { meApiCallCount++; if (request.headers.get("Authorization") === "Bearer expired-token") { return new HttpResponse(null, { status: 401 }); } return HttpResponse.json(mockUser); }), - http.post(`${API_URL}/api/auth/refresh/`, () => { + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => { _refreshApiCallCount++; return HttpResponse.json({ access: "refreshed-token" }); }), @@ -94,13 +94,13 @@ describe("response interceptor", () => { useAuthStore.getState().setAuth("expired-token", mockUser); server.use( - http.get(`${API_URL}/api/auth/me/`, ({ request }) => { + http.get(`${VITE_API_URL}/api/auth/me/`, ({ request }) => { if (request.headers.get("Authorization") === "Bearer expired-token") { return new HttpResponse(null, { status: 401 }); } return HttpResponse.json(mockUser); }), - http.post(`${API_URL}/api/auth/refresh/`, () => + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => HttpResponse.json({ access: "refreshed-token" }), ), ); @@ -115,14 +115,14 @@ describe("response interceptor", () => { server.use( http.get( - `${API_URL}/api/auth/me/`, + `${VITE_API_URL}/api/auth/me/`, () => new HttpResponse(JSON.stringify({ detail: "Invalid token" }), { status: 401, }), ), http.post( - `${API_URL}/api/auth/refresh/`, + `${VITE_API_URL}/api/auth/refresh/`, () => new HttpResponse(JSON.stringify({ detail: "Refresh failed" }), { status: 401, diff --git a/frontend/src/hooks/useAuth.test.ts b/frontend/src/hooks/useAuth.test.ts index 4d3d481..b397d3b 100644 --- a/frontend/src/hooks/useAuth.test.ts +++ b/frontend/src/hooks/useAuth.test.ts @@ -17,7 +17,7 @@ import { useAuth } from "./useAuth"; vi.mock("../utils/crypto"); vi.mock("../utils/keystore"); -const API_URL = "http://piku-server"; +const VITE_API_URL = "http://piku-server"; beforeEach(() => { vi.clearAllMocks(); @@ -112,7 +112,7 @@ describe("logout", () => { it("should call the logout API endpoint", async () => { let logoutCalled = false; server.use( - http.post(`${API_URL}/api/auth/logout/`, () => { + http.post(`${VITE_API_URL}/api/auth/logout/`, () => { logoutCalled = true; return HttpResponse.json({}); }), @@ -139,7 +139,7 @@ describe("logout", () => { it("should clear the auth store even if the API call fails", async () => { server.use( http.post( - `${API_URL}/api/auth/logout/`, + `${VITE_API_URL}/api/auth/logout/`, () => new HttpResponse(null, { status: 500 }), ), ); @@ -163,7 +163,7 @@ describe("initialize", () => { }); let refreshCalled = false; server.use( - http.post(`${API_URL}/api/auth/refresh/`, () => { + http.post(`${VITE_API_URL}/api/auth/refresh/`, () => { refreshCalled = true; return HttpResponse.json({ access: "new-token" }); }), @@ -194,7 +194,7 @@ describe("initialize", () => { it("should preserve the master key even if the refresh attempt fails", async () => { server.use( http.post( - `${API_URL}/api/auth/refresh/`, + `${VITE_API_URL}/api/auth/refresh/`, () => new HttpResponse(null, { status: 401 }), ), ); diff --git a/frontend/src/utils/dateFormat.ts b/frontend/src/utils/dateFormat.ts index 6b583d8..92acd02 100644 --- a/frontend/src/utils/dateFormat.ts +++ b/frontend/src/utils/dateFormat.ts @@ -1,13 +1,13 @@ -const timeFormatter = new Intl.DateTimeFormat(undefined, { +const timeFormatter = new Intl.DateTimeFormat("en-US", { timeStyle: "short", }); -const dateTimeFormatter = new Intl.DateTimeFormat(undefined, { +const dateTimeFormatter = new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short", }); -const rtf = new Intl.RelativeTimeFormat(undefined, { +const rtf = new Intl.RelativeTimeFormat("en-US", { numeric: "auto", }); diff --git a/frontend/utils/url-builder.ts b/frontend/utils/url-builder.ts new file mode 100644 index 0000000..1ba3c3f --- /dev/null +++ b/frontend/utils/url-builder.ts @@ -0,0 +1,9 @@ +export const getBaseUrl = ( + isSslEnabled: boolean, + domain: string | undefined, + port: string | undefined, +): string => { + const uriScheme = isSslEnabled ? "https" : "http"; + const baseURL = `${uriScheme}://${domain}${port ? `:${port}` : ""}`; + return baseURL; +}; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e0d851f..4a51717 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,16 +1,41 @@ +import fs from "node:fs"; +import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import { defineConfig, loadEnv } from "vite"; +import { getBaseUrl } from "./utils/url-builder"; // https://vite.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, "../", ""); + const isSslEnabled = env.SSL_ENABLED === "true"; + let sslCerts: { key: Buffer; cert: Buffer } | undefined; + + if (isSslEnabled) { + sslCerts = { + key: fs.readFileSync( + path.resolve(__dirname, "../certs/localhost-key.pem"), + ), + cert: fs.readFileSync(path.resolve(__dirname, "../certs/localhost.pem")), + }; + } + + const baseApiUrl = getBaseUrl( + isSslEnabled, + env.BACKEND_DOMAIN, + env.BACKEND_PORT, + ); + console.log(baseApiUrl); return { envDir: "../", plugins: [react(), tailwindcss()], + define: { + "import.meta.env.VITE_API_URL": JSON.stringify(baseApiUrl), + }, server: { port: Number(env.FRONTEND_PORT), host: env.FRONTEND_DOMAIN, + https: isSslEnabled ? sslCerts : undefined, }, }; }); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 0b301d2..4b9fc24 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ test: { env: { VITE_API_URL: "http://piku-server", + TZ: "Asia/Kolkata", }, include: ["**/*.test.ts"], environment: "jsdom", diff --git a/logs/tmp/piku_e2e_backend.log b/logs/tmp/piku_e2e_backend.log new file mode 100644 index 0000000..de8546e --- /dev/null +++ b/logs/tmp/piku_e2e_backend.log @@ -0,0 +1,180 @@ +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on https://127.0.0.1:8001 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 411-535-418 +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:15] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:16] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:16] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:17] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:19] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:19] "GET /api/auth/activate/YmFlZTYyNTgtOTcxMi00ZjFmLWE1YTgtYzBiOGMwODdkY2Zi/d755fh-d81b8ac647be32c14f996ab09e783392/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "GET /api/auth/activate/YzJjM2NkOWUtZmIxYS00MGI2LWFiM2EtYTQwYmI5MDJjZGY2/d755fh-a2be30e77657af68697d0b7be375e5ab/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "GET /api/auth/activate/Yzc1MDFhMTctY2EzOC00YTY0LTkyNmYtNWYyZjgzNDUyZGQ1/d755fh-677d8f6662cce0e74bb9a776663617a9/ HTTP/1.1" 200 - +/var/home/atom/Documents/code/pi ku/backend/.venv/lib64/python3.14/site-packages/jwt/api_jwt.py:147: InsecureKeyLengthWarning: The HMAC key is 27 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2. + return self._jws.encode( +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:20] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:20] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +/var/home/atom/Documents/code/pi ku/backend/.venv/lib64/python3.14/site-packages/jwt/api_jwt.py:365: InsecureKeyLengthWarning: The HMAC key is 27 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2. + decoded = self.decode_complete( +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/auth/activate/YjI4MjczYWMtYWNlNC00OGRlLWJmZDItZmMyMTJiZjM2MDZl/d755fh-7728ee068008d7513a64dbe6282954f3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:21] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "PUT /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:22] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/e8f47036-6e57-41f5-b057-8ea712589a73/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "PUT /api/letters/e8f47036-6e57-41f5-b057-8ea712589a73/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:23] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "OPTIONS /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:24] "PUT /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/77cbf618-f4e2-4d33-87c2-60e5c93c9711/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:26] "GET /api/letters/4e2af91a-1651-4bcd-85b5-c469ee4a73e3/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:30] "POST /api/auth/refresh/ HTTP/1.1" 401 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:30] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:31] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:32] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:34] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:35] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:35] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "POST /api/auth/register/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "OPTIONS /api/auth/register/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:36] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:36] "GET /api/auth/activate/M2M0YzFiNTItMjE5Mi00Y2VmLWIwZmItMDlkNDg5NWE4NWU0/d755fw-9c2c43d45732c1cd5ccbef20d3dc4181/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "POST /api/auth/register/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:37] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:37] "GET /api/auth/activate/MjdiM2E1M2ItOTZlYS00Y2Y1LWFhMmQtZThjY2ZkNGQ5ZjQ1/d755fw-e60e3d0b66162c75962beea97a40b4c7/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:38] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:39] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "OPTIONS /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "PUT /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 201 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:40] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:40] "GET /api/auth/activate/MDkwNTg5MDctNjAzOS00NDgwLTlkYTktNmUxOWE5ZTBjODJh/d755g0-546e5f4ee3ed111de64f011ac8d02836/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:41] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +Unauthorized: /api/auth/refresh/ +127.0.0.1 - - [16/Apr/2026 18:45:41] "POST /api/auth/refresh/ HTTP/1.1" 401 - +127.0.0.1 - - [16/Apr/2026 18:45:41] "GET /api/auth/activate/MzAxODRjMWEtMTYxZC00ZTczLThlMWMtM2FmY2RmZDg2NThk/d755g1-2ae1dec8cbac57d223c0cd206a8741a8/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "OPTIONS /api/letters/5dec1a15-1d19-47de-b5a5-21bc1845f90a/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:42] "PUT /api/letters/5dec1a15-1d19-47de-b5a5-21bc1845f90a/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "POST /api/auth/login/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "OPTIONS /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:43] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:46] "GET /api/letters/351f0e2c-e10f-4851-9836-bc387758dbad/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "OPTIONS /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "PUT /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 201 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:47] "GET /api/letters/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:49] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:49] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "POST /api/auth/refresh/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/auth/me/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +127.0.0.1 - - [16/Apr/2026 18:45:51] "GET /api/letters/e95e2a4c-811a-45b1-9bbd-dafdb180a88f/ HTTP/1.1" 200 - +Starting with SSL on 127.0.0.1:8001... +/usr/lib64/python3.14/multiprocessing/resource_tracker.py:396: UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown: {'/mp-uf_ylwyc'} + warnings.warn( diff --git a/scripts/run-e2e.sh b/scripts/run-e2e.sh index 4b18b8a..c0e7951 100755 --- a/scripts/run-e2e.sh +++ b/scripts/run-e2e.sh @@ -1,98 +1,65 @@ #!/bin/bash set -e -# Get absolute path of project root -PROJECT_ROOT=$(pwd) +# Use podman if available. Not everyone has it +CONTAINER_BIN=$(command -v podman || command -v docker) +if [ -z "$CONTAINER_BIN" ]; then + echo "Sorry, you need either podman or docker installed to run this script." + exit 1 +fi -# Configuration -ENV_FILE="$PROJECT_ROOT/.env.e2e" +if [ "$CI" = "true" ]; then + CONTAINER_BIN=$(command -v docker) +fi + +echo "Using $CONTAINER_BIN for container operations..." + +ENV_FILE="./.env.e2e" if [ -f "$ENV_FILE" ]; then - echo "[INFO] Loading configuration from $ENV_FILE..." + echo "Loading settings..." set -a source "$ENV_FILE" set +a -elif [ "$CI" != "true" ]; then - echo "[ERROR] $ENV_FILE not found! Please create it for local testing (use .env.e2e.example as template)." - exit 1 else - echo "[INFO] Running in CI mode (using direct environment variables)..." + echo "Error: Configuration file $ENV_FILE is missing!!" + exit 1 fi -# Map E2E variables to Django expected names -# In CI, these should be set via GitHub Actions env variables -export DB_NAME=${E2E_DB_DB:-piku_e2e} -export DB_USER=${E2E_DB_USER:-piku_test} -export DB_PASSWORD=${E2E_DB_PASS:-piku_test} -export DB_HOST=${E2E_DB_HOST:-localhost} -export DB_PORT=${E2E_DB_PORT:-5433} -export E2E_BACKEND_PORT=${E2E_BACKEND_PORT:-8001} - -echo "[START] Initializing E2E Test Environment..." - -# 1. Cleanup / Start Services (Skip in CI) -if [ "$CI" != "true" ]; then - if podman ps -a --format "{{.Names}}" | grep -q "^$E2E_DB_NAME$"; then - echo "[CLEANUP] Removing existing container $E2E_DB_NAME..." - podman rm -f $E2E_DB_NAME - fi - - echo "[DB] Starting disposable Postgres on port $DB_PORT..." - podman run --name $E2E_DB_NAME \ - -e POSTGRES_DB=$DB_NAME \ - -e POSTGRES_USER=$DB_USER \ - -e POSTGRES_PASSWORD=$DB_PASSWORD \ - -p $DB_PORT:5432 \ - -d docker.io/library/postgres:16-alpine > /dev/null - - echo "[DB] Waiting for Postgres to be ready..." - until podman exec $E2E_DB_NAME pg_isready -U $DB_USER > /dev/null 2>&1; do - sleep 1 - done - echo "[DB] Postgres is ready." -fi - -# Trap to ensure cleanup +# This cleans up containers. Very useful for local e2e to free system resources immediately. cleanup() { - echo "[CLEANUP] Stopping services..." - if [ "$CI" != "true" ]; then - podman rm -f $E2E_DB_NAME || true - fi - if [ ! -z "$BACKEND_PID" ]; then - kill "$BACKEND_PID" 2>/dev/null || true - fi + echo "Cleaning up..." + $CONTAINER_BIN rm -f "$DB_NAME" 2>/dev/null || true + [ -n "$BACKEND_PID" ] && kill "$BACKEND_PID" 2>/dev/null || true } trap cleanup EXIT -# 2. Prepare Backend -echo "[BACKEND] Running database migrations..." -export PIKU_ENV_FILE="$ENV_FILE" -(cd backend && uv run manage.py migrate --noinput) +echo "Starting Database and Mail server..." +COMPOSE_BIN="$(command -v docker-compose || true)" -echo "[BACKEND] Starting server on port $E2E_BACKEND_PORT..." -(cd backend && uv run manage.py runserver $E2E_BACKEND_PORT) > /tmp/piku_e2e_backend.log 2>&1 & -BACKEND_PID=$! - -echo "[BACKEND] Waiting for server to respond..." -until curl -s http://localhost:$E2E_BACKEND_PORT > /dev/null; do - sleep 1 - if ! kill -0 $BACKEND_PID 2>/dev/null; then - echo "[ERROR] Backend failed to start. Logs:" - cat /tmp/piku_e2e_backend.log - exit 1 - fi -done -echo "[BACKEND] Server is ready." - -# 3. Run Playwright -export VITE_API_URL="http://localhost:$E2E_BACKEND_PORT" - -if [ "$CI" = "true" ]; then - echo "[TEST] Running Playwright Tests (CI)..." - (cd frontend && bun run test:e2e --project=chromium "$@") +if echo "$CONTAINER_BIN" | grep -q "podman"; then + podman compose -f "./docker-compose.e2e.yml" up -d +elif [ -n "$COMPOSE_BIN" ]; then + "$COMPOSE_BIN" -f "./docker-compose.e2e.yml" up -d else - echo "[TEST] Running Playwright Tests in Distrobox..." - (cd frontend && distrobox-enter --name ubuntu-24.04 -- env VITE_API_URL=$VITE_API_URL bun run test:e2e --project=chromium "$@") + docker compose -f "./docker-compose.e2e.yml" up -d fi -echo "[SUCCESS] E2E Tests Completed." +# postgress will take some time, so we wait, and no race condition. Also, no point in logging this output +until $CONTAINER_BIN exec "$DB_NAME" pg_isready -U "${DB_USER:-test}" >/dev/null 2>&1; do + echo "Waiting for DB..." + sleep 2 +done + +echo "Starting Backend..." +mkdir -p ./tmp/logs +(cd backend && uv run manage.py migrate) +(cd backend && uv run manage.py serve) > ./tmp/logs/backend.log 2>&1 & +BACKEND_PID=$! + +if [ "$CI" = "true" ]; then + cd frontend && bun run test:e2e "$@" +else + # Because playwright decided not to support Fedora :) + cd frontend && distrobox-enter --name ubuntu-24.04 -- bun run test:e2e "$@" +fi diff --git a/scripts/start.sh b/scripts/start.sh index 1cb9678..bcdb0ec 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,4 +1,4 @@ #!/bin/bash (podman compose up -d) & -(cd backend && uv run manage.py runserver) & +(cd backend && uv run manage.py serve) & (cd frontend && bun run dev)