Skip to content

Conversation

@arkid15r
Copy link
Collaborator

@arkid15r arkid15r commented Sep 10, 2025

Proposed change

Refactor API.

Checklist

  • I've read and followed the contributing guidelines.
  • I've run make check-test locally; all checks and tests passed.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Summary by CodeRabbit

  • New Features

    • Introduced REST API v0 with unified endpoints for chapters, committees, members, organizations, events, issues, projects, releases, and repositories.
    • Added GraphQL support for API keys (queries and mutations).
    • Added a public sitemap route.
  • Changes

    • API base path updated to /api/v0/ (previously /api/v1/).
    • Authentication continues via API key header; underlying class renamed with no action required.
    • Backend consolidation of API components and migration of API key data to a new app/table (no user-facing impact).

Walkthrough

Creates a new apps.api Django app (AppConfig, model, migration), migrates ApiKey-related admin/GraphQL/REST code from nest/owasp/github into apps.api, introduces REST v0 with unified routers and ApiKeyHeader auth, updates settings/urls to use /api/v0/, adjusts lint config, and updates tests accordingly.

Changes

Cohort / File(s) Summary
App scaffolding & init
backend/apps/api/__init__.py, backend/apps/api/apps.py, backend/apps/api/rest/__init__.py, backend/apps/api/rest/auth/__init__.py
Add apps.api package, AppConfig, and REST/auth package initializers (mostly docstrings).
Admin wiring
backend/apps/api/admin/__init__.py, backend/apps/api/admin/api_key.py, backend/apps/nest/admin/__init__.py
Re-export ApiKeyAdmin from apps.api; update import paths; remove ApiKeyAdmin re-export from nest admin.
Models, migrations & exports
backend/apps/api/models/__init__.py, backend/apps/api/models/api_key.py, backend/apps/api/migrations/0001_initial.py, backend/apps/nest/migrations/0006_delete_apikey.py, backend/apps/nest/models/__init__.py, backend/apps/nest/models/user.py
Move ApiKey model to apps.api (db_table -> api_keys), add initial migration, delete nest's ApiKey via migration, update package exports and typing imports.
GraphQL: nodes/queries/mutations
backend/apps/api/internal/nodes/api_key.py, backend/apps/api/internal/queries/*, backend/apps/api/internal/mutations/*, backend/apps/nest/api/internal/mutations/__init__.py, backend/settings/graphql.py
Add ApiMutations and ApiKeyQueries, change imports from apps.nest→apps.api, update mixin inheritance to include ApiMutations and remove NestQuery usage.
REST v0 routers & auth
backend/apps/api/rest/v0/__init__.py, backend/apps/api/rest/auth/api_key.py, backend/settings/urls.py, proxy/staging.conf
Introduce REST v0 with ROUTERS mapping and dynamic registration, switch auth usage to ApiKeyHeader, change API version to v0, route site to /api/v0/, update nginx staging to api/v0, and add sitemap route.
Settings & tooling
backend/settings/base.py, backend/settings/api/__init__.py, backend/pyproject.toml
Add "apps.api" to LOCAL_APPS, remove a docstring, and broaden Ruff ignore patterns to apps/api/rest/**/*.py.
Docstring cleanup
backend/apps/github/api/rest/__init__.py, backend/apps/github/api/rest/v1/__init__.py, backend/apps/owasp/api/rest/__init__.py, backend/apps/owasp/api/rest/v1/__init__.py
Remove or clear module-level docstrings (no code changes).
Tests
backend/tests/apps/api/**, backend/tests/apps/nest/**, backend/tests/apps/api/rest/v0/**, backend/tests/apps/api/rest/auth/api_key_test.py
Update imports/patch targets from apps.nest/others → apps.api.*, add tests for ApiKeyNode and ApiKeyQueries, rename/adjust auth tests to ApiKeyHeader, and update router/schema test imports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • kasya
  • abhayymishraa

Pre-merge checks (2 passed, 1 inconclusive)

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description Check ❓ Inconclusive The description merely states “Refactor API.” which, while on-topic, is overly generic and lacks sufficient context or detail about the specific changes introduced in the pull request. Please expand the description to include a brief overview of the major modifications—such as creation of the new apps/api structure, migration to v0 endpoints, and key file reorganizations—to give reviewers clearer insight into the scope and impact of the changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Refactor API to a separate app, add v0” succinctly summarizes the core change of moving the API into its own Django app and introducing a new v0 version, making the main intent clear and specific for reviewers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ark/api-v0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
backend/tests/apps/api/rest/v0/member_test.py (1)

47-48: Fix parsing of ISO datetimes with 'Z'.

datetime.fromisoformat does not accept a 'Z' suffix in many Python versions. Replace 'Z' with '+00:00' in expectations to avoid failures.

Apply:

-        assert member.created_at == datetime.fromisoformat(member_data["created_at"])
-        assert member.updated_at == datetime.fromisoformat(member_data["updated_at"])
+        assert member.created_at == datetime.fromisoformat(member_data["created_at"].replace("Z", "+00:00"))
+        assert member.updated_at == datetime.fromisoformat(member_data["updated_at"].replace("Z", "+00:00"))
backend/tests/apps/api/rest/v0/issue_test.py (1)

38-39: Handle 'Z' timezone in expectations.

Same issue: fromisoformat won't parse 'Z'. Normalize to '+00:00'.

Apply:

-        assert schema.created_at == datetime.fromisoformat(issue_data["created_at"])
-        assert schema.updated_at == datetime.fromisoformat(issue_data["updated_at"])
+        assert schema.created_at == datetime.fromisoformat(issue_data["created_at"].replace("Z", "+00:00"))
+        assert schema.updated_at == datetime.fromisoformat(issue_data["updated_at"].replace("Z", "+00:00"))
backend/tests/apps/api/rest/v0/organization_test.py (1)

32-33: Normalize 'Z' timezone in expectations.

Avoid fromisoformat('...Z') failures by replacing with '+00:00'.

Apply:

-        assert schema.created_at == datetime.fromisoformat(organization_data["created_at"])
-        assert schema.updated_at == datetime.fromisoformat(organization_data["updated_at"])
+        assert schema.created_at == datetime.fromisoformat(organization_data["created_at"].replace("Z", "+00:00"))
+        assert schema.updated_at == datetime.fromisoformat(organization_data["updated_at"].replace("Z", "+00:00"))
backend/apps/api/internal/mutations/api_key.py (1)

52-56: Handle naive datetimes to avoid TypeError in expiry check.

Comparing a naive expires_at to timezone-aware now() raises at runtime. Normalize before comparison.

Apply this diff:

-        if expires_at <= timezone.now():
+        # Normalize to aware datetime before comparison.
+        now = timezone.now()
+        if timezone.is_naive(expires_at):
+            expires_at = timezone.make_aware(expires_at, timezone.get_current_timezone())
+        if expires_at <= now:
             return CreateApiKeyResult(
                 ok=False, code="INVALID_DATE", message="Expiry date must be in future"
             )
backend/apps/api/rest/v0/__init__.py (1)

83-89: API root says v1 but this is v0.

User-facing mismatch; fix the welcome banner.

Apply this diff:

-        "message": "Welcome to the OWASP Nest API v1",
+        "message": "Welcome to the OWASP Nest API v0",
🧹 Nitpick comments (23)
backend/apps/api/rest/auth/__init__.py (1)

1-1: Re-export ApiKeyHeader for a stable import path.

Enable from apps.api.rest.auth import ApiKeyHeader to avoid deep imports.

-"""OWASP REST API."""
+"""OWASP REST API auth package."""
+
+# Public re-exports for stable import paths.
+from .api_key import ApiKeyHeader
+
+__all__ = ["ApiKeyHeader"]
backend/apps/api/rest/__init__.py (1)

1-1: Expose default REST version alias from the package.

Handy for callers to discover the current version without importing v0 directly.

-"""OWASP REST API."""
+"""OWASP REST API package."""
+
+# Convenience re-export of the current default version.
+from .v0 import ROUTERS as V0_ROUTERS
+
+__all__ = ["V0_ROUTERS"]
backend/pyproject.toml (1)

119-123: Narrow the Ruff ignore scope to reduce blanket suppressions.

Limiting to v0 helps surface issues in future versions while keeping current ergonomics.

-lint.per-file-ignores."apps/api/rest/**/*.py" = [
+lint.per-file-ignores."apps/api/rest/v0/**/*.py" = [
   "ARG001", # https://docs.astral.sh/ruff/rules/unused-function-argument/
   "B008",   # https://docs.astral.sh/ruff/rules/function-call-in-default-argument/
 ]

If only a few endpoints trigger B008/ARG001, consider local # noqa instead.

backend/apps/api/apps.py (1)

6-9: Optional: set a human-friendly verbose name for the app.

Purely cosmetic in admin.

 class ApiConfig(AppConfig):
     """API app config."""
 
     name = "apps.api"
+    verbose_name = "API"
backend/apps/api/migrations/0001_initial.py (1)

21-43: Consider adding pragmatic indexes for common queries.

If you frequently list active keys or clean up expired/revoked keys, add indexes to keep scans cheap.

Example follow-up migration:

+class Migration(migrations.Migration):
+    dependencies = [("api", "0001_initial")]
+    operations = [
+        migrations.AddIndex(
+            model_name="apikey",
+            index=models.Index(fields=["user", "is_revoked"], name="ak_user_revoked_idx"),
+        ),
+        migrations.AddIndex(
+            model_name="apikey",
+            index=models.Index(fields=["expires_at"], name="ak_expires_at_idx"),
+        ),
+    ]

If your hot paths differ (e.g., filter by hash prefix), tailor indexes accordingly.

backend/apps/api/internal/queries/api_key.py (1)

20-30: Optional: return a concrete list or optimize queryset usage.

If downstream expects a list (not a lazy queryset), wrap with list(); if staying queryset, consider selecting only required fields to reduce payload.

Example:

-        return info.context.request.user.active_api_keys.order_by("-created_at")
+        return list(
+            info.context.request.user.active_api_keys
+            .only("uuid", "name", "created_at", "expires_at", "is_revoked")
+            .order_by("-created_at")
+        )
backend/tests/apps/api/rest/v0/committee_test.py (1)

29-30: Parse 'Z' timestamps portably
datetime.fromisoformat rejects 'Z' in some Python versions; normalize via .replace("Z", "+00:00"). Apply:

-    assert committee.created_at == datetime.fromisoformat(committee_data["created_at"])
-    assert committee.updated_at == datetime.fromisoformat(committee_data["updated_at"])
+    assert committee.created_at == datetime.fromisoformat(committee_data["created_at"].replace("Z", "+00:00"))
+    assert committee.updated_at == datetime.fromisoformat(committee_data["updated_at"].replace("Z", "+00:00"))

No other fromisoformat(... "Z" ...) occurrences found in backend/tests/.

backend/apps/api/internal/nodes/api_key.py (1)

12-17: Consider exposing last_used_at.
If helpful to clients, include last_used_at in the node fields.

Apply this diff:

     fields=[
         "created_at",
         "is_revoked",
         "expires_at",
         "name",
         "uuid",
+        "last_used_at",
     ],
backend/tests/apps/api/rest/v0/project_test.py (1)

31-33: Update test assertions to compare enum values and handle ‘Z’ timestamps (backend/tests/apps/api/rest/v0/project_test.py:31–33)

-    assert project.level == project_data["level"]
-    assert project.created_at == datetime.fromisoformat(project_data["created_at"])
-    assert project.updated_at == datetime.fromisoformat(project_data["updated_at"])
+    assert getattr(project.level, "value", project.level) == project_data["level"]
+    assert project.created_at == datetime.fromisoformat(project_data["created_at"].replace("Z", "+00:00"))
+    assert project.updated_at == datetime.fromisoformat(project_data["updated_at"].replace("Z", "+00:00"))
backend/tests/apps/api/rest/v0/chapter_test.py (1)

32-33: Parse 'Z' timestamps portably.
Normalize 'Z' to '+00:00' for stable parsing.

Apply this diff:

-    assert chapter.created_at == datetime.fromisoformat(chapter_data["created_at"])
-    assert chapter.updated_at == datetime.fromisoformat(chapter_data["updated_at"])
+    assert chapter.created_at == datetime.fromisoformat(chapter_data["created_at"].replace("Z", "+00:00"))
+    assert chapter.updated_at == datetime.fromisoformat(chapter_data["updated_at"].replace("Z", "+00:00"))
backend/apps/api/admin/api_key.py (1)

21-27: Make sensitive fields read-only in admin.
Prevents accidental edits to identifiers/timestamps.

Apply this diff:

     list_filter = ("is_revoked",)
     ordering = ("-created_at",)
+    readonly_fields = ("uuid", "hash", "created_at", "updated_at", "last_used_at")
     search_fields = (
backend/apps/api/models/__init__.py (1)

1-1: Add all for explicit export.
Small packaging nicety.

Apply this diff:

 from .api_key import ApiKey
+
+__all__ = ["ApiKey"]
backend/apps/api/internal/queries/__init__.py (1)

6-9: Class name may be confusing here.
Inside apps.api, NestQuery reads oddly; consider ApiQuery in a follow-up (with aliases if needed).

backend/apps/api/admin/__init__.py (1)

1-1: Re-export looks good; add all for clarity.

Apply this diff:

 from .api_key import ApiKeyAdmin
+
+__all__ = ["ApiKeyAdmin"]
backend/tests/apps/api/rest/v0/release_test.py (1)

1-1: Use parse_datetime for ISO-8601 with Z.
Prevents cross-version flakiness and keeps tz-aware behavior.

-from datetime import datetime
+from datetime import datetime
+from django.utils.dateparse import parse_datetime
@@
-        assert schema.created_at == datetime.fromisoformat(release_data["created_at"])
-        assert schema.published_at == datetime.fromisoformat(release_data["published_at"])
+        assert schema.created_at == parse_datetime(release_data["created_at"])
+        assert schema.published_at == parse_datetime(release_data["published_at"])

Also applies to: 30-31

backend/apps/api/internal/mutations/__init__.py (1)

1-10: Export the public symbol explicitly.
Helps static tools and importers.

 """API app mutations."""
 
 import strawberry
 
 from .api_key import ApiKeyMutations
 
 
 @strawberry.type
 class ApiMutations(ApiKeyMutations):
     """API mutations."""
+
+__all__ = ["ApiMutations"]
backend/tests/apps/api/models/api_key_test.py (1)

25-31: Remove flaky randomness assertion.
Asserting two secrets.token_urlsafe() calls differ tests the stdlib, not your method; theoretically flaky.

     def test_generate_raw_key(self):
         """Test that generate_raw_key creates a sufficiently long, random string."""
         raw_key = ApiKey.generate_raw_key()
         assert isinstance(raw_key, str)
         assert len(raw_key) > 40
-        assert secrets.token_urlsafe(32) != secrets.token_urlsafe(32)
backend/apps/api/rest/auth/api_key.py (1)

11-34: Docstring return contract mismatches behavior.
Method returns an ApiKey or raises 401; it never returns None. Align the docstring, and optionally document the 401.

 class ApiKeyHeader(APIKeyHeader):
     """Custom API key authentication class for Ninja."""
@@
     def authenticate(self, request, key: str) -> ApiKey:
-        """Authenticate the API key from the request header.
+        """Authenticate the API key from the request header.
@@
-        Returns:
-            APIKey: The APIKey object if the key is valid, otherwise None.
+        Returns:
+            ApiKey: The ApiKey object if the key is valid.
+
+        Raises:
+            HttpError: 401 if the header is missing or the key is invalid.

Confirm downstream views expect request.auth (ApiKey) and not request.user. If they need request.user, consider returning api_key.user or attaching it to request for convenience.

backend/tests/apps/api/rest/v0/urls_test.py (1)

3-13: Avoid coupling to private attributes and object identity in router tests.
Accessing main_router._routers and asserting identity with "is" makes tests brittle. Prefer public APIs (e.g., inspecting main_router.urls or registered paths) or compare prefixes only.

-        registered_router_map = dict(main_router._routers)
+        registered_router_map = dict(main_router._routers)  # consider a helper that uses public API
@@
-            assert registered_router_map[prefix] is expected_router_instance
+            assert prefix in registered_router_map  # reduce reliance on object identity

If a public accessor exists in apps.api.rest.v0 (e.g., ROUTERS or a getter), switch to that.

backend/settings/urls.py (1)

14-14: Dropping /api/v1/ is a breaking change—consider a temporary alias/redirect.
Minimize client disruption while refactoring.

-from apps.api.rest.v0 import api as api_v0
+from apps.api.rest.v0 import api as api_v0
+# Optional temporary alias to keep v1 working during migration:
+# from apps.api.rest.v0 import api as api_v1
@@
-    path("api/v0/", api_v0.urls),
+    path("api/v0/", api_v0.urls),
+    # Optional: keep legacy path for one release cycle
+    # path("api/v1/", api_v1.urls),

Confirm external consumers and docs; if v1 is intentionally removed now, add a deprecation note in CHANGELOG and update API docs.

Also applies to: 26-26

backend/tests/apps/api/internal/nodes/api_key_test.py (1)

27-34: Make UUID type assertion resilient to Strawberry’s UUID scalar.

Across Strawberry versions, UUID may surface as strawberry.scalars.UUID. Broaden the assertion to accept both.

Apply this diff:

+import strawberry
@@
-        assert fields_map["uuid"].type is uuid.UUID
+        # Accept both built-in and Strawberry UUID scalar.
+        assert fields_map["uuid"].type in {uuid.UUID, getattr(strawberry.scalars, "UUID", uuid.UUID)}
backend/tests/apps/api/internal/mutations/api_key_test.py (1)

36-77: Add negative-path tests for validation.

Cover input validation branches to lock behavior before/after refactors: blank name, overlong name, and past/naive expiry.

Example tests to add:

def test_create_api_key_invalid_name_blank(api_key_mutations):
    info = mock_info()
    result = api_key_mutations.create_api_key(info, name="  ", expires_at=timezone.now() + timedelta(days=1))
    assert not result.ok and result.code == "INVALID_NAME"

def test_create_api_key_name_too_long(api_key_mutations):
    info = mock_info()
    long_name = "x" * (MAX_ACTIVE_KEYS + 512)  # or MAX_WORD_LENGTH + 1 if available here
    result = api_key_mutations.create_api_key(info, name=long_name, expires_at=timezone.now() + timedelta(days=1))
    assert not result.ok and result.code == "INVALID_NAME"

@patch("apps.api.internal.mutations.api_key.ApiKey.create")
def test_create_api_key_invalid_expiry_in_past(mock_create, api_key_mutations):
    info = mock_info()
    past = timezone.now() - timedelta(days=1)
    result = api_key_mutations.create_api_key(info, name="n", expires_at=past)
    mock_create.assert_not_called()
    assert not result.ok and result.code == "INVALID_DATE"

Also applies to: 59-77, 78-99

backend/apps/api/internal/mutations/api_key.py (1)

14-14: Cross-app permission import—confirm intended dependency.

If the goal is to fully decouple into apps.api, consider relocating or re-exporting IsAuthenticated under apps.api to avoid reaching into apps.nest.

Would you like me to open a follow-up PR to move/re-export this permission?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 923f129 and de9c175.

📒 Files selected for processing (48)
  • backend/apps/api/__init__.py (1 hunks)
  • backend/apps/api/admin/__init__.py (1 hunks)
  • backend/apps/api/admin/api_key.py (1 hunks)
  • backend/apps/api/apps.py (1 hunks)
  • backend/apps/api/internal/mutations/__init__.py (1 hunks)
  • backend/apps/api/internal/mutations/api_key.py (3 hunks)
  • backend/apps/api/internal/nodes/api_key.py (1 hunks)
  • backend/apps/api/internal/queries/__init__.py (1 hunks)
  • backend/apps/api/internal/queries/api_key.py (1 hunks)
  • backend/apps/api/migrations/0001_initial.py (1 hunks)
  • backend/apps/api/models/__init__.py (1 hunks)
  • backend/apps/api/models/api_key.py (2 hunks)
  • backend/apps/api/rest/__init__.py (1 hunks)
  • backend/apps/api/rest/auth/__init__.py (1 hunks)
  • backend/apps/api/rest/auth/api_key.py (1 hunks)
  • backend/apps/api/rest/v0/__init__.py (1 hunks)
  • backend/apps/github/api/rest/__init__.py (0 hunks)
  • backend/apps/github/api/rest/v1/__init__.py (0 hunks)
  • backend/apps/nest/admin/__init__.py (0 hunks)
  • backend/apps/nest/api/internal/mutations/__init__.py (1 hunks)
  • backend/apps/nest/migrations/0006_delete_apikey.py (1 hunks)
  • backend/apps/nest/models/__init__.py (0 hunks)
  • backend/apps/nest/models/user.py (1 hunks)
  • backend/apps/owasp/api/rest/__init__.py (0 hunks)
  • backend/apps/owasp/api/rest/v1/__init__.py (0 hunks)
  • backend/pyproject.toml (1 hunks)
  • backend/settings/api/__init__.py (0 hunks)
  • backend/settings/base.py (1 hunks)
  • backend/settings/graphql.py (2 hunks)
  • backend/settings/urls.py (1 hunks)
  • backend/tests/apps/api/internal/mutations/api_key_test.py (6 hunks)
  • backend/tests/apps/api/internal/nodes/api_key_test.py (1 hunks)
  • backend/tests/apps/api/internal/queries/api_key_test.py (1 hunks)
  • backend/tests/apps/api/models/api_key_test.py (4 hunks)
  • backend/tests/apps/api/rest/auth/api_key_test.py (2 hunks)
  • backend/tests/apps/api/rest/v0/chapter_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/committee_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/event_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/issue_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/label_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/member_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/organization_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/project_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/release_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/repository_test.py (1 hunks)
  • backend/tests/apps/api/rest/v0/urls_test.py (1 hunks)
  • backend/tests/apps/nest/api/internal/nodes/api_keys_test.py (1 hunks)
  • backend/tests/apps/nest/api/internal/queries/api_keys_test.py (1 hunks)
💤 Files with no reviewable changes (7)
  • backend/settings/api/init.py
  • backend/apps/nest/models/init.py
  • backend/apps/nest/admin/init.py
  • backend/apps/github/api/rest/init.py
  • backend/apps/owasp/api/rest/init.py
  • backend/apps/github/api/rest/v1/init.py
  • backend/apps/owasp/api/rest/v1/init.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-13T05:55:46.436Z
Learnt from: Rajgupta36
PR: OWASP/Nest#1717
File: backend/apps/mentorship/graphql/mutations/program.py:166-166
Timestamp: 2025-07-13T05:55:46.436Z
Learning: In the OWASP Nest mentorship GraphQL mutations, Strawberry GraphQL automatically converts between Django Program instances and ProgramNode types, so mutations can return Program instances directly without manual conversion even when typed as ProgramNode, similar to the Module/ModuleNode pattern.

Applied to files:

  • backend/settings/graphql.py
📚 Learning: 2025-07-11T15:57:56.648Z
Learnt from: Rajgupta36
PR: OWASP/Nest#1717
File: backend/apps/mentorship/graphql/queries/module.py:39-50
Timestamp: 2025-07-11T15:57:56.648Z
Learning: In the OWASP Nest mentorship GraphQL queries, Strawberry GraphQL automatically converts between Django Module instances and ModuleNode types, so methods can return Module instances directly without manual conversion even when typed as ModuleNode.

Applied to files:

  • backend/settings/graphql.py
🧬 Code graph analysis (33)
backend/apps/api/internal/nodes/api_key.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/internal/queries/__init__.py (1)
backend/apps/api/internal/queries/api_key.py (1)
  • ApiKeyQueries (11-30)
backend/tests/apps/api/internal/mutations/api_key_test.py (2)
backend/apps/api/internal/mutations/api_key.py (3)
  • ApiKeyMutations (40-107)
  • CreateApiKeyResult (29-36)
  • RevokeApiKeyResult (20-25)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/admin/__init__.py (1)
backend/apps/api/admin/api_key.py (1)
  • ApiKeyAdmin (8-27)
backend/tests/apps/api/rest/v0/event_test.py (1)
backend/apps/api/rest/v0/event.py (1)
  • EventSchema (18-25)
backend/tests/apps/api/rest/v0/urls_test.py (1)
backend/apps/api/rest/v0/__init__.py (1)
  • api_root (84-89)
backend/apps/api/models/__init__.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/tests/apps/api/rest/v0/committee_test.py (1)
backend/apps/api/rest/v0/committee.py (1)
  • CommitteeSchema (18-24)
backend/tests/apps/api/rest/v0/chapter_test.py (1)
backend/apps/api/rest/v0/chapter.py (1)
  • ChapterSchema (33-40)
backend/tests/apps/api/internal/nodes/api_key_test.py (1)
backend/apps/api/internal/nodes/api_key.py (1)
  • ApiKeyNode (19-20)
backend/tests/apps/nest/api/internal/nodes/api_keys_test.py (1)
backend/apps/api/internal/nodes/api_key.py (1)
  • ApiKeyNode (19-20)
backend/tests/apps/api/internal/queries/api_key_test.py (3)
backend/apps/api/internal/queries/api_key.py (3)
  • ApiKeyQueries (11-30)
  • active_api_key_count (15-17)
  • api_keys (20-30)
backend/tests/apps/nest/api/internal/queries/api_keys_test.py (1)
  • mock_info (8-16)
backend/apps/nest/models/user.py (1)
  • active_api_keys (46-51)
backend/tests/apps/api/rest/v0/repository_test.py (1)
backend/apps/api/rest/v0/repository.py (1)
  • RepositorySchema (18-24)
backend/tests/apps/api/rest/v0/label_test.py (1)
backend/apps/api/rest/v0/label.py (1)
  • LabelSchema (27-32)
backend/tests/apps/nest/api/internal/queries/api_keys_test.py (1)
backend/apps/api/internal/queries/api_key.py (1)
  • ApiKeyQueries (11-30)
backend/tests/apps/api/rest/v0/organization_test.py (1)
backend/apps/api/rest/v0/organization.py (1)
  • OrganizationSchema (28-36)
backend/apps/api/internal/mutations/__init__.py (1)
backend/apps/api/internal/mutations/api_key.py (1)
  • ApiKeyMutations (40-107)
backend/apps/nest/models/user.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/tests/apps/api/rest/v0/issue_test.py (1)
backend/apps/api/rest/v0/issue.py (1)
  • IssueSchema (28-36)
backend/tests/apps/api/rest/v0/member_test.py (1)
backend/apps/api/rest/v0/member.py (1)
  • MemberSchema (30-47)
backend/tests/apps/api/rest/v0/project_test.py (1)
backend/apps/api/rest/v0/project.py (1)
  • ProjectSchema (28-35)
backend/apps/nest/migrations/0006_delete_apikey.py (1)
backend/apps/api/migrations/0001_initial.py (1)
  • Migration (10-50)
backend/apps/nest/api/internal/mutations/__init__.py (1)
backend/apps/nest/api/internal/mutations/user.py (1)
  • UserMutations (38-114)
backend/tests/apps/api/rest/v0/release_test.py (1)
backend/apps/api/rest/v0/release.py (1)
  • ReleaseSchema (24-31)
backend/apps/api/internal/queries/api_key.py (1)
backend/apps/api/internal/nodes/api_key.py (1)
  • ApiKeyNode (19-20)
backend/tests/apps/api/models/api_key_test.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/internal/mutations/api_key.py (2)
backend/apps/api/internal/nodes/api_key.py (1)
  • ApiKeyNode (19-20)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/rest/auth/api_key.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/migrations/0001_initial.py (2)
backend/apps/nest/migrations/0006_delete_apikey.py (1)
  • Migration (6-15)
backend/apps/nest/migrations/0002_apikey.py (1)
  • Migration (10-48)
backend/apps/api/admin/api_key.py (1)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/tests/apps/api/rest/auth/api_key_test.py (2)
backend/apps/api/models/api_key.py (1)
  • ApiKey (16-94)
backend/apps/api/rest/auth/api_key.py (1)
  • ApiKeyHeader (11-33)
backend/settings/graphql.py (2)
backend/apps/api/internal/mutations/__init__.py (1)
  • ApiMutations (9-10)
backend/apps/api/internal/queries/api_key.py (1)
  • ApiKeyQueries (11-30)
backend/apps/api/rest/v0/__init__.py (1)
backend/apps/api/rest/auth/api_key.py (1)
  • ApiKeyHeader (11-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run frontend unit tests
  • GitHub Check: Run backend tests
  • GitHub Check: Run frontend e2e tests
🔇 Additional comments (28)
backend/apps/api/__init__.py (1)

1-1: LGTM.

Docstring-only package init is fine.

backend/settings/base.py (1)

55-55: LGTM.

Adding apps.api to LOCAL_APPS aligns with the refactor.

backend/apps/nest/models/user.py (1)

14-14: LGTM.

TYPE_CHECKING import updated to the new apps.api path; active_api_keys still matches related_name="api_keys".

backend/apps/nest/api/internal/mutations/__init__.py (1)

9-10: Decoupling Nest mutations from ApiKey looks right.

Nice separation: NestMutations now focuses on user-only concerns.

Confirm the new ApiMutations root in apps.api is wired into the GraphQL schema (schema assembly/urls) and that any client-facing changes are documented/changelogged.

backend/tests/apps/nest/api/internal/queries/api_keys_test.py (1)

5-5: Import path update looks correct. No remaining references to the old apps.nest.api.internal.queries.api_key import were found.

backend/tests/apps/nest/api/internal/nodes/api_keys_test.py (1)

4-4: Import path aligned with new namespace.

Good move to apps.api.internal.nodes.api_key.

backend/tests/apps/api/rest/v0/label_test.py (1)

3-3: LGTM on v0 import.

Import path now points to apps.api.rest.v0.label as intended.

backend/tests/apps/api/rest/v0/event_test.py (1)

5-5: LGTM on v0 import.

No functional changes to the test.

backend/tests/apps/api/rest/v0/member_test.py (1)

5-5: LGTM on v0 import.

backend/tests/apps/api/rest/v0/issue_test.py (1)

5-5: LGTM on v0 import.

backend/apps/api/internal/queries/api_key.py (2)

6-6: Import path for ApiKeyNode updated — OK.

Consistent with the API move.


7-7: Keep IsAuthenticated import unchanged
The permissions.py class lives only under apps/nest/api/internal, so the existing import path is correct.

backend/tests/apps/api/rest/v0/organization_test.py (1)

5-5: LGTM on v0 import.

backend/tests/apps/api/rest/v0/committee_test.py (1)

5-5: Import path update looks good.
Aligns with the v0 move.

backend/apps/api/internal/nodes/api_key.py (1)

6-6: LGTM on model import path change.
Matches the model relocation into apps.api.

backend/tests/apps/api/rest/v0/project_test.py (1)

5-5: Import path update looks good.
Consistent with v0.

backend/tests/apps/api/rest/v0/chapter_test.py (1)

5-5: Import path update looks good.
Matches the v0 namespace.

backend/apps/api/admin/api_key.py (1)

1-6: Admin import/docstring updates look good.
Path now points to apps.api.models; docstring clarified.

backend/apps/api/internal/queries/__init__.py (1)

3-3: Import path update looks good.
ApiKeyQueries now sourced from apps.api.

backend/tests/apps/api/rest/v0/repository_test.py (1)

5-5: Import path update looks correct.
Matches the new v0 module location.

backend/tests/apps/api/rest/v0/release_test.py (1)

5-5: Import path update looks correct.
Aligned with apps.api.rest.v0.release.

backend/apps/api/internal/mutations/__init__.py (1)

8-10: GraphQL mutation root wiring LGTM.
Subclassing ApiKeyMutations under @strawberry.type is standard.

backend/tests/apps/api/models/api_key_test.py (1)

9-9: Imports and patch targets updated correctly.
Targets match the relocated ApiKey model; decorator argument order is correct.

Also applies to: 41-43, 59-59, 71-71

backend/apps/api/rest/auth/api_key.py (1)

8-8: Import path change is correct.
Now points to apps.api.models.api_key.

backend/tests/apps/api/internal/queries/api_key_test.py (1)

1-48: Solid unit tests for resolvers.
Mocks are minimal and assertions target the correct queryset methods.

backend/tests/apps/api/internal/nodes/api_key_test.py (1)

10-26: Good whitelist to prevent leaking sensitive fields.

Explicit equality on the field set helps ensure fields like "hash" never slip into the schema.

backend/settings/graphql.py (1)

21-27: GraphQL root composition looks consistent with the refactor.

ApiMutations/ApiKeyQueries are correctly wired; NestQuery removal is reflected. Keep an eye on name collisions across inherited roots.

Also applies to: 31-39

backend/tests/apps/api/rest/auth/api_key_test.py (1)

6-17: Auth tests updated cleanly to ApiKeyHeader.

Patch targets and behaviors (success, invalid, missing) are correct and focused.

Also applies to: 22-51, 52-67

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
proxy/staging.conf (1)

31-36: Optionally pass X-Forwarded-Host for correct absolute URL building behind proxies.

Django may rely on X-Forwarded-Host when USE_X_FORWARDED_HOST=True. This is harmless even if unset.

Apply this diff to both backend proxy locations (and optionally the frontend one):

         proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-Host $host;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
backend/apps/api/rest/v0/__init__.py (4)

19-37: Normalize router prefixes (drop leading “/”) to avoid accidental double slashes.

Ninja joins prefixes; leading slashes aren’t required and can produce // in some setups/logging.

-    "/chapters": chapter_router,
-    "/committees": committee_router,
-    "/members": member_router,
-    "/organizations": organization_router,
-    "/events": event_router,
-    "/issues": issue_router,
-    "/projects": project_router,
-    "/releases": release_router,
-    "/repositories": repository_router,
+    "chapters": chapter_router,
+    "committees": committee_router,
+    "members": member_router,
+    "organizations": organization_router,
+    "events": event_router,
+    "issues": issue_router,
+    "projects": project_router,
+    "releases": release_router,
+    "repositories": repository_router,

44-45: Externalize throttle rate into settings.

Hardcoding “10/s” makes tuning per-environment harder; read from settings with a sane default.

-    "throttle": [AuthRateThrottle("10/s")],
+    "throttle": [AuthRateThrottle(getattr(settings, "API_THROTTLE_RATE", "10/s"))],

46-46: Single source of truth for API version.

Consider moving the version to settings (e.g., API_VERSION) or package init to avoid drift.

Would you like a quick patch to read it from settings with a default?


87-87: Use dynamic version in the welcome message.

Keeps the message in sync with api.version automatically.

-        "message": "Welcome to the OWASP Nest API v0",
+        "message": f"Welcome to the OWASP Nest API v{api.version}",
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de9c175 and 11bcf8e.

📒 Files selected for processing (2)
  • backend/apps/api/rest/v0/__init__.py (2 hunks)
  • proxy/staging.conf (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/apps/api/rest/v0/__init__.py (1)
backend/apps/api/rest/auth/api_key.py (1)
  • ApiKeyHeader (11-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run frontend e2e tests
  • GitHub Check: Run frontend unit tests
  • GitHub Check: Run backend tests
🔇 Additional comments (2)
backend/apps/api/rest/v0/__init__.py (2)

41-41: Confirm docs/openapi auth posture when setting global ApiKeyHeader.

With global auth, your /api/v0/openapi.json (and thus Swagger UI) may require the API key. If public docs are intended, explicitly exempt docs/openapi or override auth per-route.

Would you like me to propose a minimal pattern to keep routes authenticated while leaving docs/openapi public?


7-17: Router consolidation/imports look good.

The relative import for chapter_router and unified ROUTERS map simplify wiring. No issues spotted.

@arkid15r arkid15r requested a review from kasya September 10, 2025 01:52
@arkid15r arkid15r marked this pull request as ready for review September 10, 2025 01:52
@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/apps/api/rest/v0/__init__.py (1)

19-37: Docs cleanup echo: replace stale /api/v1 links with /api/v0.

Reiterating earlier review: update any remaining doc references to match these prefixes (chapters, committees, members, organizations, events, issues, projects, releases, repositories, and labels if added).

🧹 Nitpick comments (2)
backend/apps/api/rest/v0/__init__.py (2)

7-15: Use a consistent import style (avoid mixing absolute and relative).

Prefer absolute imports for all v0 modules for clarity and grep-ability.

Apply:

-from .chapter import router as chapter_router
+from apps.api.rest.v0.chapter import router as chapter_router

Also applies to: 17-17


41-41: Avoid auth error info leakage (generic 401s).

Current ApiKeyHeader differentiates missing vs invalid keys. Prefer a single 401 message to reduce enumeration signals.

Change in backend/apps/api/rest/auth/api_key.py:

-        if not key:
-            raise HttpError(HTTPStatus.UNAUTHORIZED, "Missing API key in 'X-API-Key' header")
+        if not key:
+            raise HttpError(HTTPStatus.UNAUTHORIZED, "Invalid or missing API key")
@@
-        raise HttpError(HTTPStatus.UNAUTHORIZED, "Invalid API key")
+        raise HttpError(HTTPStatus.UNAUTHORIZED, "Invalid or missing API key")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11bcf8e and 2bec149.

📒 Files selected for processing (1)
  • backend/apps/api/rest/v0/__init__.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/apps/api/rest/v0/__init__.py (1)
backend/apps/api/rest/auth/api_key.py (1)
  • ApiKeyHeader (11-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run frontend e2e tests
  • GitHub Check: Run backend tests
  • GitHub Check: Run frontend unit tests
  • GitHub Check: CodeQL (javascript-typescript)
🔇 Additional comments (3)
backend/apps/api/rest/v0/__init__.py (3)

1-1: LGTM: clear module docstring.


87-87: LGTM: root banner matches v0 path.


39-47: Harden Swagger docs persistence

  • In backend/apps/api/rest/v0/init.py, change
    docs: Swagger(settings={"persistAuthorization": True})

    docs: Swagger(settings={"persistAuthorization": settings.IS_LOCAL_ENVIRONMENT})
  • Confirm any tests or code references expecting an API version are updated to "version": "0.2.0".

Copy link
Collaborator

@kasya kasya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great update 🚀

@arkid15r arkid15r added this pull request to the merge queue Sep 10, 2025
Merged via the queue into main with commit ad007b7 Sep 10, 2025
25 checks passed
@arkid15r arkid15r deleted the ark/api-v0 branch September 10, 2025 17:16
arkid15r added a commit that referenced this pull request Sep 14, 2025
* Refactor API to a separate app, add v0

* Add v0 migration files

* Set v0 version
@coderabbitai coderabbitai bot mentioned this pull request Oct 5, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants