feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(feedback): encrypt free-text input + admin read surface (#520) - #525

Merged
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback
Aug 6, 2026
Merged

feat(feedback): encrypt free-text input + admin read surface (#520)#525
AndresL230 merged 6 commits into
mainfrom
feat/522-b-520-feedback

Conversation

@AndresL230

@AndresL230AndresL230 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Closes#520.

  • feedback.comment/topic, issue_reports.topic/description encrypted at write
  • NEW admin surface (the app had no reader; the dashboard goes blind): GET /api/admin/feedback + /api/admin/issue-reports and a portal feedback tab
  • backfill runners, encrypted seed rows, roundtrip tests, ciphertext-oracle manifest entries
  • rollout-safe: reads tolerate pre-backfill plaintext rows (tested)

Stacked on #519's ADR PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin Feedback tab for viewing feedback and issue reports.
    • Displays ratings, comments, metadata, screenshots, descriptions, and user names.
    • Added admin-only endpoints for retrieving recent feedback and issue reports.
  • Security
    • Sensitive feedback and issue-report text is now encrypted at rest and securely decrypted for authorized viewing.
  • Bug Fixes
    • Preserved compatibility with existing plaintext feedback records.
  • Documentation
    • Documented new feedback and issue-report testing identifiers.

@supabase

supabaseBot commented Aug 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Encrypted feedback administration

Layer / File(s)Summary
Write encryption and backfill
backend/routes/feedback.py, backend/db/backfill_encryption.py, backend/db/seed_local_rich.py, backend/tests/*, backend/e2e_oracles/gather.py, CLAUDE.md
Feedback and issue-report free-text fields are encrypted on write, seeded as ciphertext, included in backfill runners, and covered by round-trip tests and encryption manifests.
Admin listing endpoints
backend/routes/admin.py, backend/tests/test_admin_routes.py
Admin-only endpoints return bounded, newest-first feedback and issue-report records with decrypted text and user display names.
Admin feedback tab
frontend/src/lib/api.ts, frontend/src/components/screens/Admin.tsx, docs/frontend-testids.md
The Admin screen adds feedback and issue-report API types, loaders, navigation, rendering, states, and test-ID documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
participant AdminUI
participant AdminAPI
participant AdminRoutes
participant FeedbackTables
AdminUI->>AdminAPI: Request feedback and issue reports
AdminAPI->>AdminRoutes: GET /api/admin/feedback and /api/admin/issue-reports
AdminRoutes->>FeedbackTables: Read bounded newest-first records
FeedbackTables-->>AdminRoutes: Feedback and issue-report rows
AdminRoutes-->>AdminAPI: Decrypted enriched records
AdminAPI-->>AdminUI: Renderable admin responses
Loading

Possibly related PRs

  • SaplingLearn/Sapling#526: Directly implements the encrypted feedback and issue-report changes described by the linked encryption-gap work.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Linked Issues check⚠️ WarningThe PR covers encryption, decryption, backfill, and tests, but the Canopy table update is unshown and Admin.tsx retains a negative-rating crash risk.Update the Canopy sapling-infrastructure encrypted-columns table and clamp feedback.rating in both repeat() calls before merging.
Docstring Coverage⚠️ WarningDocstring coverage is 11.11% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the primary encryption and admin read-surface changes.
Description check✅ PassedThe description states the purpose, key changes, linked issue, and testing coverage; missing template headings do not prevent review.
Out of Scope Changes check✅ PassedThe code, documentation, seed, backfill, API, UI, and test changes all support the linked encryption and admin-surface objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/522-b-520-feedback

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging1e36394Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:24 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

Found 1 issue:

  1. "★".repeat(f.rating) in the new FeedbackTab clamps only the second repeatfeedback.rating is an unbounded int (no Pydantic constraint on SubmitFeedbackBody.rating, no DB CHECK in 0026_ops.sql), so one hand-crafted POST /api/feedback with rating: -1 makes String.prototype.repeat throw RangeError and crashes the whole admin Feedback tab for every admin.

<span>{f.type}</span>
<span>{"★".repeat(f.rating)}{"☆".repeat(Math.max(0,5-f.rating))}</span>
{f.topic&&<span>{f.topic}</span>}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@AndresL230
AndresL230 changed the base branch from feat/522-a-519-newsletter-adr to mainAugust 6, 2026 01:20
AndresL230and others added 6 commits August 5, 2026 21:21
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ption (#520)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f.rating is an unbounded int; repeat() on the unclamped complement
throws RangeError for rating > 5 and crashes the admin FeedbackTab.
Clamp once per row and reuse for both the filled and empty glyphs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-b-520-feedback branch from 3052b3a to 1e36394CompareAugust 6, 2026 01:21

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/admin.py`:
- Around line 514-527: Update list_feedback and list_issue_reports in
backend/routes/admin.py to return responses with the Cache-Control: no-store
header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fbea67dd-ec82-42bd-95c4-a5b3a3a48885

📥 Commits

Reviewing files that changed from the base of the PR and between 104b573 and 1e36394.

📒 Files selected for processing (12)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/admin.py
  • backend/routes/feedback.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_admin_routes.py
  • backend/tests/test_feedback_routes.py
  • docs/frontend-testids.md
  • frontend/src/components/screens/Admin.tsx
  • frontend/src/lib/api.ts

Comment on lines +514 to +527
@router.get("/feedback")
def list_feedback(request: Request, limit: int = 200):
require_admin(request)
rows = table("feedback").select(
"id,user_id,type,rating,selected_options,comment,session_id,topic,created_at",
order="created_at.desc",
limit=max(1, min(int(limit), 500)),
) or []
names = get_display_names([r["user_id"] for r in rows])
for r in rows:
r["comment"] = decrypt_if_present(r.get("comment"))
r["topic"] = decrypt_if_present(r.get("topic"))
r["user_name"] = names.get(r["user_id"], "")
return {"feedback": rows}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== admin route relevant sections =="
sed -n '480,555p' backend/routes/admin.py
echoecho"== auth guard require_admin =="
sed -n '90,125p' backend/services/auth_guard.py
echoecho"== tests around feedback/reports =="
sed -n '640,725p' backend/tests/test_admin_routes.py
echoecho"== search for Cache-Control headers in admin/auth/test files =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|response\\.headers|PlainResponse|JSONResponse|Response" backend/routes/admin.py backend/services/auth_guard.py backend/tests/test_admin_routes.py backend 2>/dev/null | head -200
echoecho"== app middleware / response headers snippets =="
rg -n "Cache-Control|no-store|requires-cors-preflight|CORSMiddleware|include_subschemas|response\\.headers|PlainResponse|JSONResponse|Response" backend -g '*.py'| head -200
echoecho"== route imports and router registration =="
rg -n "from routes|include_route|admin_router|auth_guard|require_admin|Cache-Control|Clear-Site" backend -g '*.py'

Repository: SaplingLearn/Sapling

Length of output: 50376


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== imports in backend/services/request_context.py =="
sed -n '1,80p' backend/services/request_context.py
echoecho"== request context around response handling =="
sed -n '50,175p' backend/services/request_context.py
echoecho"== main.py app and middleware =="
sed -n '130,235p' backend/main.py
echoecho"== focused tests/search for Cache-Control across python files =="
rg -n --glob '*.py''"Cache-Control":|Cache-Control|no-store|private' backend/services backend/tests backend/main.py backend/routes/admin.py backend/routes/admin_analytics.py
echoecho"== deterministic header behavior from router return values =="
python3 - <<'PY'# Read the relevant route bodies as text and perform a lightweight# control-flow check: does either GET handler pass a Response/JSONResponse,# or assign response.headers["Cache-Control"] before returning dict?from pathlib import Pathsrc = Path("backend/routes/admin.py").read_text()for name in ("list_feedback", "list_issue_reports"): idx = src.index(f"def {name}") next_def = src.find("\ndef ", idx + 1) body = src[idx:next_def] if next_def != -1 else src[idx:] print(f"--- {name} ---") print("returns dict/json:", "return {" in body or 'return {"' in body) print("has explicit Cache-Control assignment:", 'headers["Cache-Control"]' in body) print("passes Response/JSONResponse:", any(f"class.{kw}" in body or f"({kw}" in body for kw in ("Response", "JSONResponse")))PY

Repository: SaplingLearn/Sapling

Length of output: 19598


Sensitive Data Exposure (CWE-525): Use of Web Browser Cache Containing Sensitive Information

Reachability: External

Prevent browser-cache reuse of decrypted admin responses.

require_admin() protects the request, but these decrypted responses do not disable local caching. list_feedback() and list_issue_reports() return plain dicts without setting Cache-Control: no-store, and the regression tests do not assert this header.

  • Set Cache-Control: no-store on /api/admin/feedback and /api/admin/issue-reports.
  • Add regression assertions for both endpoints in backend/tests/test_admin_routes.py.
📍 Affects 2 files
  • backend/routes/admin.py#L514-L527 (this comment)
  • backend/routes/admin.py#L530-L543
  • backend/tests/test_admin_routes.py#L658-L714
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/admin.py` around lines 514 - 527, Update list_feedback and
list_issue_reports in backend/routes/admin.py to return responses with the
Cache-Control: no-store header while preserving their existing payloads. In
backend/tests/test_admin_routes.py, extend the regression coverage for both
endpoints to assert that header is present.

@AndresL230
AndresL230 merged commit 01fea19 into mainAug 6, 2026
7 checks passed
@AndresL230
AndresL230 deleted the feat/522-b-520-feedback branch August 6, 2026 01:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Free-text user input stored in plaintext: feedback.comment and issue_reports.description

1 participant

@AndresL230