Conversation
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Greptile SummaryThis PR adds a Daily Recap sharing feature: a share button in the Flutter app generates a
Confidence Score: 4/5Merging is low-risk for production users but has a P1 Redis cleanup gap and two P2 security concerns worth addressing first. The P1 Redis leak (orphaned keys after delete) is a real defect that diverges from the established conversation-sharing cleanup pattern. The two P2 findings (implicit backfill and no rate limiting) are security-adjacent and easy to fix before merge. backend/database/daily_summaries.py (missing Redis cleanup on delete) and backend/routers/users.py (lazy backfill scope + no rate limit on public endpoint).
|
| Filename | Overview |
|---|---|
| backend/database/daily_summaries.py | Adds Redis write on create (correct) but delete_daily_summary never removes the Redis key — orphaned entries accumulate indefinitely. |
| backend/database/redis_db.py | Adds store/get helpers for summary→uid mapping; no TTL (consistent with conversation-sharing pattern), but missing a remove counterpart needed for cleanup on delete. |
| backend/routers/users.py | New public endpoint correctly falls back to Firestore for auth, but has no rate limiting; lazy backfill silently makes every read shareable. |
| app/lib/pages/settings/daily_summary_detail_page.dart | Adds share button and _shareSummary with Mixpanel tracking; URL is hardcoded to production h.omi.me. |
| app/lib/utils/analytics/mixpanel.dart | Adds dailySummaryShared tracking event — straightforward, no issues. |
| web/frontend/src/app/recaps/[id]/page.tsx | New public Next.js page renders shared recap with OG metadata; fetches with no-cache (appropriate for personal data). |
| web/frontend/src/components/memories/share-button.tsx | Client component that copies window.location.href to clipboard with visual feedback — correct and clean. |
Sequence Diagram
sequenceDiagram
participant App as Flutter App
participant Backend as Backend API
participant Redis as Redis
participant Firestore as Firestore
participant Web as Web (h.omi.me)
Note over App,Firestore: Authenticated read (lazy backfill)
App->>Backend: GET /v1/users/daily-summaries/{id} [auth]
Backend->>Firestore: get_daily_summary(uid, id)
Firestore-->>Backend: summary dict
Backend->>Redis: GET daily-summary:{id}
Redis-->>Backend: (empty on first read)
Backend->>Redis: SET daily-summary:{id} = uid
Backend-->>App: summary JSON
Note over App,Web: Share flow
App->>App: _shareSummary() → SharePlus sheet
App-->>App: URL = https://h.omi.me/recaps/{id}
Note over Web,Firestore: Public page load (no auth)
Web->>Backend: GET /v1/daily-summaries/{id}/shared
Backend->>Redis: GET daily-summary:{id}
Redis-->>Backend: uid
Backend->>Firestore: get_daily_summary(uid, id)
Firestore-->>Backend: summary dict
Backend-->>Web: summary JSON
Web-->>Web: Render public recap page
Comments Outside Diff (1)
-
backend/database/daily_summaries.py, line 124-138 (link)Redis key not cleaned up on delete
delete_daily_summaryremoves the Firestore document but never deletes thedaily-summary:{summary_id}Redis key. The public endpoint still correctly 404s (because Firestore returnsNone), but the Redis key accumulates forever. The conversation-sharing pattern in the codebase has an explicitremove_conversation_to_uidcounterpart called on deletion — daily summaries should do the same.Add a
remove_daily_summary_to_uidfunction toredis_db.py:def remove_daily_summary_to_uid(summary_id: str): r.delete(f'daily-summary:{summary_id}')
Then call it in
delete_daily_summary:def delete_daily_summary(uid: str, summary_id: str) -> bool: user_ref = db.collection('users').document(uid) summary_ref = user_ref.collection(DAILY_SUMMARIES_COLLECTION).document(summary_id) summary_ref.delete() redis_db.remove_daily_summary_to_uid(summary_id) return True
Reviews (1): Last reviewed commit: "feat(app): add share button to daily rec..." | Re-trigger Greptile
| if (summary == null) return; | ||
|
|
||
| MixpanelManager().dailySummaryShared(summaryId: widget.summaryId, date: summary.date); | ||
| final url = 'https://h.omi.me/recaps/${widget.summaryId}'; |
There was a problem hiding this comment.
The share URL is hardcoded to h.omi.me, meaning dev/staging builds always share links pointing at production. If the web frontend is deployed differently per environment, the URL should come from an environment config (e.g., Env.shareBaseUrl).
| final url = 'https://h.omi.me/recaps/${widget.summaryId}'; | |
| final url = '${Env.shareBaseUrl}/recaps/${widget.summaryId}'; |
| @router.get('/v1/daily-summaries/{summary_id}/shared', tags=['v1']) | ||
| def get_shared_daily_summary(summary_id: str): | ||
| """ | ||
| Public endpoint to retrieve a daily summary for sharing. No auth required. | ||
| """ | ||
| uid = get_daily_summary_uid(summary_id) | ||
| if not uid: | ||
| raise HTTPException(status_code=404, detail='Daily summary not found') | ||
|
|
||
| summary = daily_summaries_db.get_daily_summary(uid, summary_id) | ||
| if not summary: | ||
| raise HTTPException(status_code=404, detail='Daily summary not found') | ||
|
|
||
| return summary |
There was a problem hiding this comment.
Unauthenticated endpoint has no rate limiting
This public endpoint has no auth and no rate-limiting dependency. Any caller can probe it at high frequency — and because the Redis key is also set on every authenticated read (lazy backfill), even IDs that were never explicitly shared become discoverable. Adding a rate-limit dependency (consistent with other public-facing endpoints in this router) would reduce abuse risk.
@router.get('/v1/daily-summaries/{summary_id}/shared', tags=['v1'])
def get_shared_daily_summary(
summary_id: str,
_=Depends(auth.with_rate_limit(None, 'public_shared_summary')),
):| # Lazy backfill: ensure Redis has the uid mapping so the share link works | ||
| if not get_daily_summary_uid(summary_id): | ||
| store_daily_summary_to_uid(summary_id, uid) |
There was a problem hiding this comment.
Lazy backfill silently registers every read as "shareable"
The backfill stores a uid mapping in Redis for every summary the owner reads — even ones they never intended to share. This means any summary with at least one read is now discoverable via the public endpoint if the summary_id becomes known. Consider only writing the Redis key when the user explicitly opts into sharing (e.g., triggered by the mobile share action or a dedicated POST /share endpoint), rather than on every GET.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Summary
h.omi.me/recaps/{id})GET /v1/daily-summaries/{summary_id}/shared(no auth) with Redis uid lookup/recaps/[id]showing the recap publiclyDemo
ScreenRecording_04-15-2026.11-41-55_1.mov
🤖 Generated with Claude Code