Skip to content

feat(permissions): add LIMIT_ONE_SEASON permission#2752

Open
dangerouslaser wants to merge 7 commits intoseerr-team:developfrom
dangerouslaser:feat/limit-one-season-permission
Open

feat(permissions): add LIMIT_ONE_SEASON permission#2752
dangerouslaser wants to merge 7 commits intoseerr-team:developfrom
dangerouslaser:feat/limit-one-season-permission

Conversation

@dangerouslaser
Copy link
Copy Markdown

@dangerouslaser dangerouslaser commented Mar 23, 2026

Description

Adds a new LIMIT_ONE_SEASON permission that restricts users to requesting only one TV season at a time. This gives admins more granular control over request behavior without disabling partial requests entirely.

Changes in this PR:

  • Backend: Added Permission.LIMIT_ONE_SEASON (536870912) and SeasonLimitError. Centralized validation into MediaRequest.validateSeasonLimit(), enforced in both POST /api/v1/request and PUT /api/v1/request/:id.

  • Frontend (PermissionEdit): Added the permission toggle with requires covering standard and 4K request permissions.

  • Frontend (TvRequestModal): Season selection switches to single-select (replace) mode. The limit is derived from the effective request user (supports on-behalf requests). Quota logic accounts for replacement semantics. Uses role="radio"/role="radiogroup" for a11y when in single-season mode.

  • Partially addresses [Feature] Limit number of seasons per request #1351

How Has This Been Tested?

  • Built locally with `pnpm build` (clean compile, no type errors)
  • Deployed a local Docker build against a live Seerr instance
  • Verified the permission toggle appears in user settings for users with any TV request permission (standard + 4K)
  • Tested single-season enforcement: selecting a season replaces the previous selection, quota display remains accurate
  • Tested that admins/managers bypass the restriction as expected
  • Verified the backend rejects multi-season requests with 403 for limited users on both POST and PUT endpoints

Screenshots / Logs (if applicable)

N/A

AI Disclosure

AI was used to assist with code review feedback implementation.

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build `pnpm build`
  • Translation keys `pnpm i18n:extract`
  • Database migration (if required)

@dangerouslaser dangerouslaser requested a review from a team as a code owner March 23, 2026 14:22
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Permission.LIMIT_ONE_SEASON and enforces a single-season request rule: backend exports SeasonLimitError and validates LIMIT_ONE_SEASON users (without MANAGE_REQUESTS or ADMIN) to prevent >1 season requests; routes map the error to HTTP 403; frontend updates permission editor and TV request modal to restrict multi-season selection and messaging.

Changes

Cohort / File(s) Summary
Backend: permissions & enforcement
server/lib/permissions.ts, server/entity/MediaRequest.ts, server/routes/request.ts
Added Permission.LIMIT_ONE_SEASON; exported SeasonLimitError; added MediaRequest.validateSeasonLimit(user, seasons) and invoked it during TV request flow; route handlers map SeasonLimitError to HTTP 403 and prevent PUT updates that would add >1 season.
Frontend: permission editor
src/components/PermissionEdit/index.tsx
Added i18n keys and a new permission option limit-one-season mapped to Permission.LIMIT_ONE_SEASON, with a requires constraint tied to request-related permissions.
Frontend: TV request modal
src/components/RequestModal/TvRequestModal.tsx
Introduced isOneSeasonLimited derived from Permission.LIMIT_ONE_SEASON (excluding MANAGE_REQUESTS/ADMIN); UI shows alert and treats selection as exclusive (radio semantics), updates toggle/submit logic to send selected seasons when limited, and adjusts CTA/disabled state and table visibility accordingly.

Sequence Diagram

sequenceDiagram
    participant User as User (Limited)
    participant Modal as TV Request Modal
    participant API as POST /api/v1/request
    participant Server as MediaRequest Handler
    participant Resp as HTTP Response

    User->>Modal: select seasons (attempt >1)
    Modal->>Modal: evaluate isOneSeasonLimited (permission + not admin/manager)
    alt client restricts selection
        Modal->>Modal: enforce single selection / show alert
    else client sends payload with >1 season
        Modal->>API: POST /api/v1/request (seasons payload)
        API->>Server: validate request
        Server->>Server: MediaRequest.validateSeasonLimit(user, seasons)
        alt multiple seasons & limited
            Server->>Server: throw SeasonLimitError
            Server->>Resp: 403 Forbidden (error.message)
            Resp->>Modal: display error
        else valid single season
            Server->>Resp: 200 OK
            Resp->>Modal: success
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop through fields of code at dawn,

One season only—no more to spawn.
I guard the gate with gentle cheer,
A single choice, held close and dear.
Tiny paws, a tidy rule—🥕

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(permissions): add LIMIT_ONE_SEASON permission' clearly and accurately summarizes the main change in the PR, which is the addition of a new LIMIT_ONE_SEASON permission to restrict users to requesting one season at a time.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/components/RequestModal/TvRequestModal.tsx (1)

440-465: ⚠️ Potential issue | 🟡 Minor

Finish threading isOneSeasonLimited through the remaining quota-only branches.

The CTA state now treats limited users like partial-request mode, but the auto-approve alert and QuotaDisplay below still only key off partialRequestsEnabled. When partial requests are globally off, a limited user can still see over-limit/zero-remaining messaging based on the full unrequested season count even though selecting one season is valid.

🔧 Suggested follow-up
       ) &&
         !(
           quota?.tv.limit &&
-          !settings.currentSettings.partialRequestsEnabled &&
+          !settings.currentSettings.partialRequestsEnabled &&
+          !isOneSeasonLimited &&
           unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
         ) &&
         getAllRequestedSeasons().length < getAllSeasons().length &&
         !editRequest && (
@@
         <QuotaDisplay
           mediaType="tv"
           quota={quota?.tv}
           remaining={
-            !settings.currentSettings.partialRequestsEnabled &&
+            !settings.currentSettings.partialRequestsEnabled &&
+            !isOneSeasonLimited &&
             unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
               ? 0
               : currentlyRemaining
           }
@@
           overLimit={
-            !settings.currentSettings.partialRequestsEnabled &&
+            !settings.currentSettings.partialRequestsEnabled &&
+            !isOneSeasonLimited &&
             unrequestedSeasons.length > (quota?.tv.remaining ?? 0)
               ? unrequestedSeasons.length
               : undefined
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/RequestModal/TvRequestModal.tsx` around lines 440 - 465, The
UI still treats quota messaging and the QuotaDisplay/auto-approve alert as if
only settings.currentSettings.partialRequestsEnabled matters; thread
isOneSeasonLimited into those checks so limited users are treated like
partial-requests-enabled. Update all conditional checks that currently read
settings.currentSettings.partialRequestsEnabled (or its negation) in the
quota-only branches (including the auto-approve alert logic and the QuotaDisplay
rendering) to use (settings.currentSettings.partialRequestsEnabled ||
isOneSeasonLimited) (or its negation) and ensure unrequestedSeasons/quota
comparisons use the effective mode; keep references to isOneSeasonLimited,
quota, unrequestedSeasons, getAllRequestedSeasons, getAllSeasons,
selectedSeasons, QuotaDisplay and the auto-approve alert logic in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/entity/MediaRequest.ts`:
- Around line 438-448: The LIMIT_ONE_SEASON check is applied to requestUser in
MediaRequest.request(), which diverges from the frontend (TvRequestModal) that
derives the flag from the logged-in user and is not enforced on edits (PUT
/api/v1/request/:id); extract and centralize this logic into a reusable
validator (e.g., MediaRequest.ensureOneSeasonLimit or
MediaRequest.validateSeasonLimit) that accepts the principal performing the
action (logged-in/acting user) and the request payload (finalSeasons), use
Permission.LIMIT_ONE_SEASON and throw SeasonLimitError when violated, then call
this validator from both MediaRequest.request() and the update handler for PUT
/api/v1/request/:id so on-behalf requests use the same principal and the same
403 behavior.

In `@src/components/PermissionEdit/index.tsx`:
- Around line 190-200: The UI toggle for the permission with id
'limit-one-season' (Permission.LIMIT_ONE_SEASON) only appears when the edited
user has Permission.REQUEST or Permission.REQUEST_TV; update its requires entry
so it also includes 4K-only request permissions (Permission.REQUEST_4K and
Permission.REQUEST_4K_TV) in the permissions array (keeping type: 'or') so users
who only have 4K request bits can be configured from the admin UI.

---

Outside diff comments:
In `@src/components/RequestModal/TvRequestModal.tsx`:
- Around line 440-465: The UI still treats quota messaging and the
QuotaDisplay/auto-approve alert as if only
settings.currentSettings.partialRequestsEnabled matters; thread
isOneSeasonLimited into those checks so limited users are treated like
partial-requests-enabled. Update all conditional checks that currently read
settings.currentSettings.partialRequestsEnabled (or its negation) in the
quota-only branches (including the auto-approve alert logic and the QuotaDisplay
rendering) to use (settings.currentSettings.partialRequestsEnabled ||
isOneSeasonLimited) (or its negation) and ensure unrequestedSeasons/quota
comparisons use the effective mode; keep references to isOneSeasonLimited,
quota, unrequestedSeasons, getAllRequestedSeasons, getAllSeasons,
selectedSeasons, QuotaDisplay and the auto-approve alert logic in your changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d98ff30-f512-4fd5-af74-81f00ac814b3

📥 Commits

Reviewing files that changed from the base of the PR and between 865396f and 523d8d2.

📒 Files selected for processing (5)
  • server/entity/MediaRequest.ts
  • server/lib/permissions.ts
  • server/routes/request.ts
  • src/components/PermissionEdit/index.tsx
  • src/components/RequestModal/TvRequestModal.tsx

@dangerouslaser dangerouslaser force-pushed the feat/limit-one-season-permission branch from 523d8d2 to ca988a6 Compare March 23, 2026 14:46
@delagomme
Copy link
Copy Markdown

Thanks, small but impactful feature with a high cost-benefit ratio. This would also partially address #1351. It would be great to see this included in an upcoming release fairly soon, as many users are eagerly waiting for this 👍

@dangerouslaser dangerouslaser force-pushed the feat/limit-one-season-permission branch from 9e4e331 to f2abeca Compare April 1, 2026 13:08
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/RequestModal/TvRequestModal.tsx`:
- Around line 555-557: The season selection UI in TvRequestModal uses checkbox
semantics even when isOneSeasonLimited is true, which should be radio semantics;
update the rendering where (!settings.currentSettings.partialRequestsEnabled ||
isOneSeasonLimited) controls visibility so that when isOneSeasonLimited you
render a radiogroup container and each season item uses role="radio" (and
aria-checked) instead of checkbox input/role, and ensure keyboard/ARIA behaviors
match radio semantics; locate the season-row rendering logic in TvRequestModal
(references: settings.currentSettings.partialRequestsEnabled,
isOneSeasonLimited) and switch the markup/role/aria attributes accordingly for
the single-season mode (also apply the same change at the other occurrences
noted around the other blocks).
- Around line 284-288: The isOneSeasonLimited flag currently uses the signed-in
user's permissions via hasPermission(Permission.LIMIT_ONE_SEASON), which is
wrong when a manager/admin is submitting on behalf of requestOverrides.user;
change the logic to derive this flag from the effective/request owner instead:
resolve the selected user id (requestOverrides.user or the modal's selected
user), fetch or compute that user's permissions (e.g., via the same
hasPermission call scoped to that user or by loading their permission set), and
then set isOneSeasonLimited based on whether that target user has
Permission.LIMIT_ONE_SEASON and lacks MANAGE_REQUESTS/ADMIN; update any UI
gating (multi-season select) to use this computed flag to prevent a 403 on
submit.
- Around line 309-311: The one-season branch (isOneSeasonLimited) replaces
selection via setSelectedSeasons([seasonNumber]) but the quota checks and
messaging still assume selections only grow; update the guard that uses
currentlyRemaining and the quota message logic that uses
unrequestedSeasons.length so they respect replacement semantics when
isOneSeasonLimited is true (i.e., compute remaining and over-limit using the
effective post-replacement set instead of the additive logic), and mirror this
same change in the other quota branch referenced around the 457-465 area; use
the existing symbols isOneSeasonLimited, setSelectedSeasons, currentlyRemaining,
unrequestedSeasons, and partialRequestsEnabled to locate and adjust the checks
and messages.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 226717d2-4f62-4209-bad6-c836df5dee07

📥 Commits

Reviewing files that changed from the base of the PR and between ca988a6 and f2abeca.

📒 Files selected for processing (5)
  • server/entity/MediaRequest.ts
  • server/lib/permissions.ts
  • server/routes/request.ts
  • src/components/PermissionEdit/index.tsx
  • src/components/RequestModal/TvRequestModal.tsx
✅ Files skipped from review due to trivial changes (2)
  • server/lib/permissions.ts
  • server/routes/request.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/entity/MediaRequest.ts

Add a new user permission that restricts series requests to one season
at a time. When enabled, the request modal switches to single-select
mode for seasons, hides the select-all toggle, and the backend rejects
multi-season requests with a 403. Admins and users with Manage Requests
are exempt from the restriction.

This is intended to be paired with an external prefetch service (e.g.
Prefetcharr) that automatically requests additional seasons based on
user watch progress.
- Centralize season limit validation into MediaRequest.validateSeasonLimit()
- Derive isOneSeasonLimited from effective request user (requestOverrides)
- Fix quota logic to allow season replacement in single-season mode
- Use radio semantics (role=radio/radiogroup) for a11y in single-season mode
@dangerouslaser dangerouslaser force-pushed the feat/limit-one-season-permission branch from 1568e09 to 483b1f6 Compare April 6, 2026 11:28
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.

2 participants