Skip to content

feat share daily recap as public link#6660

Open
krushnarout wants to merge 10 commits intomainfrom
feat/share-daily-recap
Open

feat share daily recap as public link#6660
krushnarout wants to merge 10 commits intomainfrom
feat/share-daily-recap

Conversation

@krushnarout
Copy link
Copy Markdown
Member

Summary

  • Adds a share button to the Daily Recap detail page that generates a public link (h.omi.me/recaps/{id})
  • New backend public endpoint GET /v1/daily-summaries/{summary_id}/shared (no auth) with Redis uid lookup
  • New web page at /recaps/[id] showing the recap publicly
  • Lazy Redis backfill on first authenticated fetch for older summaries

Demo

Screenshot 1

Screenshot 2

ScreenRecording_04-15-2026.11-41-55_1.mov

🤖 Generated with Claude Code

@krushnarout krushnarout linked an issue Apr 15, 2026 that may be closed by this pull request
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 15, 2026

Greptile Summary

This PR adds a Daily Recap sharing feature: a share button in the Flutter app generates a h.omi.me/recaps/{id} link, backed by a new unauthenticated backend endpoint that resolves the summary owner via Redis, and a new Next.js public page renders the recap with OG metadata.

  • P1 – Redis key not cleaned up on delete: delete_daily_summary removes the Firestore document but never deletes the daily-summary:{summary_id} Redis key. Zombie keys accumulate indefinitely; the conversation-sharing pattern has an explicit remove_conversation_to_uid counterpart that should be mirrored here.
  • P2 – Every authenticated read silently registers a summary as shareable: the lazy backfill in GET /v1/users/daily-summaries/{summary_id} writes a Redis uid mapping on each read, making every viewed summary discoverable via the public endpoint even if the user never clicked Share.
  • P2 – Public endpoint has no rate limiting: the unauthenticated /shared endpoint should have a rate-limit dependency consistent with other public endpoints in this router.

Confidence Score: 4/5

Merging 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).

Security Review

  • Implicit sharing via lazy backfill (backend/routers/users.py:1050): the uid→summary Redis mapping is written on every authenticated GET, not only when the user explicitly shares. Any summary the owner has ever opened is discoverable via the unauthenticated endpoint if its summary_id is known.
  • No rate limiting on unauthenticated endpoint (backend/routers/users.py:1070): GET /v1/daily-summaries/{summary_id}/shared has no auth and no rate-limit dependency, enabling high-frequency probing.
  • Full summary payload exposed: the endpoint returns the raw Firestore dict, which includes fields not rendered by the web UI (people_mentioned, memorable_moments, overall_sentiment, tomorrow_focus). This is acceptable for an intentional share but reinforces the concern about implicit sharing via backfill.

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. backend/database/daily_summaries.py, line 124-138 (link)

    P1 Redis key not cleaned up on delete

    delete_daily_summary removes the Firestore document but never deletes the daily-summary:{summary_id} Redis key. The public endpoint still correctly 404s (because Firestore returns None), but the Redis key accumulates forever. The conversation-sharing pattern in the codebase has an explicit remove_conversation_to_uid counterpart called on deletion — daily summaries should do the same.

    Add a remove_daily_summary_to_uid function to redis_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}';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Hardcoded production URL

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).

Suggested change
final url = 'https://h.omi.me/recaps/${widget.summaryId}';
final url = '${Env.shareBaseUrl}/recaps/${widget.summaryId}';

Comment thread backend/routers/users.py
Comment on lines +1070 to +1083
@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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 security 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')),
):

Comment thread backend/routers/users.py
Comment on lines +1050 to +1052
# 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 security 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.

Sign up for free to 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.

Add share button for Daily Recaps

1 participant