Summary
Keep exercise metadata public and cacheable, but move paid content behind dynamic entitlement checks. Public catalog/detail responses will not expose text or audioFileId. Users start an exercise through a protected begin endpoint that checks Free/Pro access and returns a short-lived audio URL only when allowed. Submissions send propositionId + userText; backend loads the original text from the database.
Key Changes
- Public metadata API:
- Change public
GET /proposition/{id} to return a safe DTO only: id, title, topic, level, image, duration, source/date, and access metadata.
- Do not return
Text or AudioFileId from public/cached endpoints.
- Keep public metadata endpoints Cloudflare-cacheable.
- Protected begin endpoint:
- Add
POST /proposition/{id}/begin with Cache-Control: no-store.
- Check access using the US-005 rule: latest 18 global exercises are Free; all older exercises require Pro.
- Treat anonymous or failed session lookup as Free.
- If access is granted, return
{ access: "granted", audioUrl, audioExpiresAtUtc, metadata }.
- If access is blocked, return
{ access: "pro_required", metadata } without text, audioFileId, or audio URL.
- Entitlement lookup:
- Propositions-service forwards incoming cookies to users-service
/users/auth/session.
isPro=true grants Pro access.
- Frontend generated API config must send credentials for dynamic begin/submit calls.
- Audio protection:
- Make the MinIO
propositions bucket private; keep images public.
- Add a file service method to create presigned GET URLs for audio objects.
- Audio presigned URLs expire after 1 hour.
- Purge/avoid Cloudflare caching for old public audio URLs after changing bucket policy.
- Frontend exercise flow:
- Exercise page loads public metadata first.
- Clicking Begin calls
/proposition/{id}/begin.
- Granted response stores
audioUrl, transitions to exercise state, and passes audioUrl to the audio component.
- Pro-required response keeps the user on the intro/paywall state and opens/shows the Pro upgrade modal.
- Audio component stops deriving URLs from
environment.minioUrl + /propositions/{audioFileId}.
- Text comparison:
- Replace client-provided
originalText with server-owned original text.
- Frontend sends
{ propositionId, userText }.
- Backend validates proposition ID, checks the same begin entitlement, loads
Proposition.Text from DB, then runs comparison.
- Response may still include original text in the correction result only after access is granted.
- Free-user standard correction:
- Free and anonymous users receive the existing deterministic/static comparison for exercises they are allowed to use.
- Free users can submit only latest-18 free exercises.
- Free submissions must not call any AI service.
- Static comparison response shape must remain compatible with the current results UI.
Test Plan
- Backend tests:
- Public proposition response excludes original text and
audioFileId.
- Free/anonymous users can begin latest 18 global exercises.
- Free/anonymous users get
pro_required for older exercises.
- Pro users can begin older exercises.
- Granted begin response includes presigned audio URL and expiry.
- Blocked begin response excludes text,
audioFileId, and audio URL.
- Submit endpoint ignores/rejects client original text and loads original text from DB.
- Submit endpoint rejects Pro-only exercise submissions from non-Pro users.
- Free/anonymous allowed submissions run static comparison only and do not call AI.
- Static comparison response remains compatible with the current frontend result components.
- Frontend tests:
- Begin button calls protected begin endpoint.
- Granted begin response transitions from intro to exercise state.
- Pro-required begin response shows Pro upgrade UI and does not render audio/writing controls.
- Audio component uses supplied
audioUrl.
- Submit sends proposition ID and user text only.
- Free-user static comparison results still render in the existing results UI.
Assumptions
- US-001 entitlement and US-005 free-window metadata exist or are implemented first.
- Audio presigned URLs use MinIO and expire after 1 hour.
- Public metadata remains safe to cache on Cloudflare.
- Direct audio protection is achieved by making the
propositions bucket private and only issuing presigned URLs after entitlement succeeds.
- US-009 is intentionally folded into this story; standard/static correction for Free users is part of the protected submit flow.
Summary
Keep exercise metadata public and cacheable, but move paid content behind dynamic entitlement checks. Public catalog/detail responses will not expose
textoraudioFileId. Users start an exercise through a protected begin endpoint that checks Free/Pro access and returns a short-lived audio URL only when allowed. Submissions sendpropositionId + userText; backend loads the original text from the database.Key Changes
GET /proposition/{id}to return a safe DTO only: id, title, topic, level, image, duration, source/date, and access metadata.TextorAudioFileIdfrom public/cached endpoints.POST /proposition/{id}/beginwithCache-Control: no-store.{ access: "granted", audioUrl, audioExpiresAtUtc, metadata }.{ access: "pro_required", metadata }without text,audioFileId, or audio URL./users/auth/session.isPro=truegrants Pro access.propositionsbucket private; keepimagespublic./proposition/{id}/begin.audioUrl, transitions to exercise state, and passesaudioUrlto the audio component.environment.minioUrl + /propositions/{audioFileId}.originalTextwith server-owned original text.{ propositionId, userText }.Proposition.Textfrom DB, then runs comparison.Test Plan
audioFileId.pro_requiredfor older exercises.audioFileId, and audio URL.audioUrl.Assumptions
propositionsbucket private and only issuing presigned URLs after entitlement succeeds.