Skip to content

feat(auth): support Plex home profile login#1594

Open
0xSysR3ll wants to merge 71 commits into
seerr-team:developfrom
0xSysR3ll:plex-profiles-support
Open

feat(auth): support Plex home profile login#1594
0xSysR3ll wants to merge 71 commits into
seerr-team:developfrom
0xSysR3ll:plex-profiles-support

Conversation

@0xSysR3ll
Copy link
Copy Markdown
Contributor

@0xSysR3ll 0xSysR3ll commented Apr 17, 2025

Description

This PR adds support for Plex Home profiles.

Authentication flow

flowchart TD
    A[User] -->|POST /auth/plex| B[Server]
    B -->|"single profile"| C["200 OK (session set)"]
    B -->|"multiple profiles"| D{{status: REQUIRES_PROFILE}}
    D --> E[Client shows profile selector]
    E -->|"POST /auth/plex/profile/select (profileId, pin?)"| F[Server]
    F -->|"needs PIN & none"| G{{status: REQUIRES_PIN}}
    F -->|"wrong PIN"| H[401 Invalid PIN]
    F -->|"imported profile"| I["200 OK (session set)"]
    F -->|"not imported & Allow new Plex logins OFF"| J[403 Access denied]
    F -->|"not imported & setting ON"| K[User created]
    K --> L[200 OK]

Loading

Note

E‑mail fallback <ownerPrefix>+<profile>@<ownerDomain> (plex.local if the owner address has no domain)

P.S : Sorry for this "duplicate" PR (see #1503), but it's a complete rework.

How Has This Been Tested?

Screenshot (if UI-related)

Plex profile selector
image
PIN entry
image
Invalid PIN
image
Profile not imported (auto-import disabled)
image
Users imported
image

Plex Profile Import

In addition to the authentication flow, this PR now also adds support for importing Plex profiles from the admin interface:

  • Admins can now import Plex profiles alongside regular Plex users
  • Duplicate detection prevents importing profiles that conflict with existing users
  • Clear visual indicators for potential duplicates
  • Improved success and error messages that properly distinguish between users and profiles
Screenshots

Plex Profile Import UI
image
Duplicate Detection
image

To-Dos

  • Make "Tautulli" feature work with plex profiles.
  • Add "Linked account" support for plex profiles.
  • Support for plex users that are part of the Plex Home.
  • Add support for manual profiles import.
  • Add support for profile's watch list.

Issues Fixed or Closed

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)

Summary by CodeRabbit

  • New Features

    • Multi-profile Plex sign-in with profile selection UI and 4-digit PIN entry for protected profiles.
    • Import Plex Home profiles as separate users, with duplicate detection and separate import results.
    • New server endpoints to list/select Plex profiles; login responses may request profile selection or PIN.
    • DB changes to store Plex profile linkage on users.
  • Refactor

    • Login flow reworked into a multi-step Plex authentication orchestration.
  • Documentation

    • Added localization for profile selection, PIN entry, import, and duplicate messaging.

Comment thread server/api/plextv.ts Fixed
Comment thread src/components/Login/PlexProfileSelector.tsx Fixed
@0xSysR3ll 0xSysR3ll force-pushed the plex-profiles-support branch from 10e5838 to f5de648 Compare April 17, 2025 23:14
Comment thread server/api/plextv.ts Fixed
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

I don't know if I did not RTFM properly but I struggled a bit with these data migration files sorry (I am using a docker env btw)...

@gauthier-th
Copy link
Copy Markdown
Member

I don't know if I did not RTFM properly but I struggled a bit with these data migration files sorry (I am using a docker env btw)...

We have a step-by-step guide in our contribution guide to generate these migrations: https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md#migrations

@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

I don't know if I did not RTFM properly but I struggled a bit with these data migration files sorry (I am using a docker env btw)...

We have a step-by-step guide in our contribution guide to generate these migrations: https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md#migrations

Yep, I read this guide but I don't know why it generated unnecessary and incorrect things. Maybe I need to review my dev env.

@gauthier-th
Copy link
Copy Markdown
Member

I don't know if I did not RTFM properly but I struggled a bit with these data migration files sorry (I am using a docker env btw)...

We have a step-by-step guide in our contribution guide to generate these migrations: https://github.com/fallenbagel/jellyseerr/blob/develop/CONTRIBUTING.md#migrations

Yep, I read this guide but I don't know why it generated unnecessary and incorrect things. Maybe I need to review my dev env.

Migrations for SQLite can be quite messy yes. IIRC SQLite doesn't support editing columns, hence the long migration files for SQLite.

@0xSysR3ll 0xSysR3ll marked this pull request as draft April 25, 2025 07:32
Comment thread server/routes/auth.ts Fixed
Comment thread server/routes/auth.ts Fixed
Comment thread server/routes/auth.ts Fixed
Comment thread server/routes/auth.ts Fixed
Comment thread server/routes/auth.ts Fixed
Comment thread server/routes/auth.ts Fixed
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

Why does the cypress-run suddenly fails ? I didn't touch anything...
I am facing the same issue on my dev env.

@0xSysR3ll 0xSysR3ll force-pushed the plex-profiles-support branch from 87fa714 to 02a2d9c Compare April 26, 2025 15:49
@fallenbagel

This comment was marked as resolved.

@0xSysR3ll

This comment was marked as resolved.

@0xSysR3ll 0xSysR3ll marked this pull request as ready for review April 26, 2025 21:46
@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

@fallenbagel
Copy link
Copy Markdown
Collaborator

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

I can push a preview tag tonight

@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

0xSysR3ll commented Apr 30, 2025

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

I can push a preview tag tonight

Awesome thanks 🔥

@edbourque0
Copy link
Copy Markdown

I'm not sure if this is the right place to ask this question (sorry if it's not) but does this pull request add the ability for each profile to request movies in jellyseerr thru their plex watchilsts ? This feature is already available but I was just wondering if this would add a "per profile" request thru each profile's watchlist.

Thank you!

@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

I'm not sure if this is the right place to ask this question (sorry if it's not) but does this pull request add the ability for each profile to request movies in jellyseerr thru their plex watchilsts ? This feature is already available but I was just wondering if this would add a "per profile" request thru each profile's watchlist.

Thank you!

I haven’t had the chance to review or test the watchlist feature yet, so I can’t confirm at the moment. However, if it doesn’t currently support this functionality, it would be great to consider integrating it.

@0xSysR3ll 0xSysR3ll force-pushed the plex-profiles-support branch from e762c9e to 6999dcd Compare May 1, 2025 15:00
@edbourque0
Copy link
Copy Markdown

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

I can push a preview tag tonight

What is the name of the tag ? I'm eager to test this

@0xSysR3ll
Copy link
Copy Markdown
Contributor Author

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

I can push a preview tag tonight

What is the name of the tag ? I'm eager to test this

I think fallenbagel forgot.
Must be busy.

@fallenbagel
Copy link
Copy Markdown
Collaborator

I believe this PR is ready for review. I haven’t identified any issues so far, and there doesn’t appear to be any further work needed at this stage. Would it be appropriate to provide a tag for testing, or is it still too early ?

I can push a preview tag tonight

What is the name of the tag ? I'm eager to test this

I think fallenbagel forgot.
Must be busy.

Yeah sorry I got busy 😅
Let me push it

@fallenbagel
Copy link
Copy Markdown
Collaborator

Preview should be available in half an hour as:
preview-plex-home-profile

@0xSysR3ll 0xSysR3ll force-pushed the plex-profiles-support branch from 73688ba to ce27178 Compare February 27, 2026 22:52
Comment thread server/routes/auth.ts Fixed
@0xSysR3ll 0xSysR3ll marked this pull request as ready for review February 27, 2026 23:07
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

Caution

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

⚠️ Outside diff range comments (1)
server/routes/auth.ts (1)

319-333: ⚠️ Potential issue | 🟡 Minor

Misleading log message: says "Jellyseerr" but context is Plex access denial.

The log message at line 320 says "creating new Jellyseerr user" but this code path is actually denying access to a Plex user without media server access. The message is misleading and contradicts the actual behavior (returning 403).

🔧 Fix the log message
       } else {
-        logger.info(
-          'Sign-in attempt from Plex user with access to the media server; creating new Jellyseerr user',
+        logger.warn(
+          'Sign-in attempt from Plex user without access to the media server',
           {
             label: 'API',
             ip: req.ip,
             email: account.email,
             plexId: account.id,
             plexUsername: account.username,
           }
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/auth.ts` around lines 319 - 333, The logger.info message is
misleading—inside the branch that returns next({ status: 403, error:
ApiErrorCode.NewPlexLoginDisabled, ... }) it incorrectly says "creating new
Jellyseerr user"; update the log text in the logger.info call (near the same
block using logger.info, req.ip, account.email, account.id, account.username and
the subsequent next(...) call) to accurately reflect that the Plex sign-in was
denied due to lack of media server access (e.g., "Sign-in attempt from Plex user
without access to the media server; denying access").
♻️ Duplicate comments (2)
server/routes/auth.ts (1)

649-668: ⚠️ Potential issue | 🟠 Major

Consider returning an error instead of silently authenticating when profile matches a regular Plex user.

When an existing user has a different plexId and is not a profile user (lines 651-654), the code logs a warning but still authenticates the user (lines 664-667). This could lead to unexpected behavior where selecting a profile logs you into an unrelated user's account if their plexProfileId happens to match.

🔒 Proposed hardening
       if (
         profileUser.plexId &&
         profileUser.plexId !== mainUser.plexId &&
         profileUser.userType !== UserType.PLEX_PROFILE
       ) {
         logger.warn('Attempted to use a regular Plex user as a profile', {
           label: 'Auth',
           profileId,
           userId: profileUser.id,
           mainUserId: mainUser.id,
         });

-        // Simply use their account without modifying it
-        if (req.session) {
-          req.session.userId = profileUser.id;
-        }
-        return res.status(200).json(profileUser.filter() ?? {});
+        return next({
+          status: 409,
+          message: 'This profile ID conflicts with an existing user account.',
+        });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/auth.ts` around lines 649 - 668, The current branch that
handles an existing user with a different plexId and userType !==
UserType.PLEX_PROFILE (symbols: profileUser, mainUser, UserType.PLEX_PROFILE,
logger.warn, req.session.userId, profileId) should not silently authenticate;
instead, stop setting req.session.userId and return a failure response (e.g.,
403 Forbidden or 400) with a clear error message and relevant identifiers in the
payload after logging the warning. Update the handler so when the condition
matches you log the incident via logger.warn and then immediately return
res.status(...).json({ error: 'Profile selection conflicts with an existing
non-profile user', profileId, userId: profileUser.id }) without mutating session
or proceeding to authenticate.
server/api/plextv.ts (1)

376-384: ⚠️ Potential issue | 🟡 Minor

Add timeout to the switchProfile axios call.

This axios.post call is missing a timeout configuration. The getProfiles method (line 282) and getUsers method (line 443) both have timeout: 10000, but this call can hang indefinitely if the Plex API is unresponsive.

🔧 Proposed fix
       const response = await axios.post(urlPath, pin ? { pin } : {}, {
         baseURL: 'https://clients.plex.tv',
+        timeout: 10000,
         headers: {
           Accept: 'application/json',
           'Content-Type': 'application/json',
           'X-Plex-Token': this.authToken,
           'X-Plex-Client-Identifier': randomUUID(),
         },
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/plextv.ts` around lines 376 - 384, The axios.post call in
switchProfile (the POST to urlPath with pin) lacks a timeout and can hang;
update the request options passed to axios.post in the switchProfile method to
include a timeout (e.g. timeout: 10000) alongside baseURL and headers so it
matches getProfiles/getUsers behavior and fails fast if the Plex API is
unresponsive.
🧹 Nitpick comments (6)
server/routes/auth.ts (2)

497-505: Redundant API call after successful PIN validation.

The getUser() call at line 498 is redundant. If validateProfilePin returns true (line 487), the profile switch already succeeded. This extra call adds unnecessary latency without providing additional security benefit.

♻️ Remove redundant call
         if (selectedProfile.protected && pin) {
           const isPinValid = await plextv.validateProfilePin(profileId, pin);

           if (!isPinValid) {
             return next({
               status: 401,
               message: 'Invalid PIN.',
               error: ApiErrorCode.InvalidPin,
             });
           }
-
-          try {
-            await plextv.getUser();
-          } catch (e) {
-            return next({
-              status: 401,
-              message: 'Invalid PIN.',
-              error: ApiErrorCode.InvalidPin,
-            });
-          }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/auth.ts` around lines 497 - 505, Remove the redundant
plextv.getUser() call and its surrounding try/catch in the route handler—if
validateProfilePin(...) already returned true and profile switching succeeded,
do not make an extra plextv.getUser() call; delete the block that calls
plextv.getUser() and returns a 401 on catch, leaving the flow to continue after
validateProfilePin succeeds (refer to validateProfilePin and plextv.getUser in
auth.ts to locate the code).

604-619: Redundant/dead code: profile lookup already performed.

The exactProfileUser lookup at lines 605-607 is unreachable. The code only reaches this block when profileUser is null (line 553), meaning the earlier lookup by plexProfileId at line 549-551 already returned no results. This second lookup with the same plexProfileId will always return null.

♻️ Remove redundant code
       } else {
-        // Then check for any other potential matches
-        const exactProfileUser = await userRepository.findOne({
-          where: { plexProfileId: profileId },
-        });
-
-        if (exactProfileUser) {
-          logger.info('Found existing profile user with exact ID match', {
-            label: 'Auth',
-            profileId,
-            userId: exactProfileUser.id,
-          });
-
-          if (req.session) {
-            req.session.userId = exactProfileUser.id;
-          }
-          return res.status(200).json(exactProfileUser.filter() ?? {});
-        } else {
           // Create a new profile user
           profileUser = new User({
             // ...
           });
-        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/auth.ts` around lines 604 - 619, Remove the redundant second
lookup and its handling: delete the const exactProfileUser = await
userRepository.findOne({ where: { plexProfileId: profileId } }); block and the
subsequent if (exactProfileUser) { ... } return; since profileUser was already
queried by plexProfileId earlier, this code is unreachable; if you intended to
reuse a found user ensure you reference profileUser (and set req.session.userId
= profileUser.id) instead.
src/components/Login/index.tsx (1)

145-161: Optimize repeated profile lookups.

The profile is looked up 5 times with .find() for the same profileId. Cache the result for cleaner, more efficient code.

♻️ Cache profile lookup
       if (response.data?.status === 'REQUIRES_PIN') {
+        const profile = profiles.find((p) => p.id === profileId);
         setShowPinEntry(true);
         setPinProfileId(profileId);
-        setPinProfileName(
-          profiles.find((p) => p.id === profileId)?.title ||
-            profiles.find((p) => p.id === profileId)?.username ||
-            'Profile'
-        );
-        setPinProfileThumb(
-          profiles.find((p) => p.id === profileId)?.thumb || null
-        );
-        setPinIsProtected(
-          profiles.find((p) => p.id === profileId)?.protected || false
-        );
-        setPinIsMainUser(
-          profiles.find((p) => p.id === profileId)?.isMainUser || false
-        );
+        setPinProfileName(profile?.title || profile?.username || 'Profile');
+        setPinProfileThumb(profile?.thumb || null);
+        setPinIsProtected(profile?.protected || false);
+        setPinIsMainUser(profile?.isMainUser || false);
         setPinError(null);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Login/index.tsx` around lines 145 - 161, The code repeatedly
calls profiles.find((p) => p.id === profileId) for the same profileId; cache the
found profile in a local variable (e.g., const profile = profiles.find(p => p.id
=== profileId)) before the block and then replace the multiple finds used by
setShowPinEntry, setPinProfileId, setPinProfileName, setPinProfileThumb,
setPinIsProtected, and setPinIsMainUser with properties read from that cached
profile (with the same fallback values), ensuring you handle the case where
profile is undefined.
server/api/plextv.ts (1)

396-410: Unreachable catch block in validateProfilePin.

The catch block will never execute because switchProfile catches all exceptions internally and returns false instead of rethrowing. The error logging at lines 404-408 is dead code.

♻️ Simplify the method
   public async validateProfilePin(
     profileId: string,
     pin: string
   ): Promise<boolean> {
-    try {
-      const success = await this.switchProfile(profileId, pin);
-      return success;
-    } catch (e) {
-      logger.error('Failed to validate Plex profile pin', {
-        label: 'Plex.tv API',
-        errorMessage: e.message,
-      });
-      return false;
-    }
+    return this.switchProfile(profileId, pin);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/plextv.ts` around lines 396 - 410, The try/catch in
validateProfilePin is dead because switchProfile already catches errors and
returns false; remove the try/catch and simply return the result of
this.switchProfile(profileId, pin) in validateProfilePin (leave switchProfile
unchanged), and delete the unreachable logger.error block so validateProfilePin
becomes a thin wrapper that returns await this.switchProfile(profileId, pin).
src/components/UserList/PlexImportModal.tsx (1)

159-188: Add type safety for response.skipped property.

The code accesses response.skipped which isn't defined in the response type from the axios call. While the optional chaining handles undefined, explicitly typing the response would improve maintainability.

♻️ Add explicit typing
-      const { data: response } = await axios.post(
+      const { data: response } = await axios.post<{
+        data: { userType: number }[];
+        skipped?: { type: string }[];
+      }>(
         '/api/v1/user/import-from-plex',
         {
           plexIds: selectedUsers,
           profileIds: selectedProfiles,
         }
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UserList/PlexImportModal.tsx` around lines 159 - 188, Define
an explicit TypeScript interface (e.g., PlexImportResponse { skipped?: { type:
string }[]; ... }) for the API payload returned in PlexImportModal and apply it
to the axios call (use axios.post<PlexImportResponse>(...) or type response.data
as PlexImportResponse) so that response.skipped is strongly typed; update any
local usages (response.skipped, skippedUsers, skippedProfiles) to rely on the
typed response and keep existing optional checks (e.g., response.skipped && ...)
to preserve runtime safety.
seerr-api.yml (1)

117-117: Model userType as an explicit enum, not description-only mapping.

Line 117 documents valid values, but schema validation/codegen won’t enforce them without an enum.

💡 Proposed OpenAPI refinement
         userType:
           type: integer
+          enum: [1, 2, 3, 4, 5]
           example: 1
           description: 'User type - 1=PLEX, 2=LOCAL, 3=JELLYFIN, 4=EMBY, 5=PLEX_PROFILE'
           readOnly: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@seerr-api.yml` at line 117, The userType schema currently only documents
allowed numeric values in its description; change it into an explicit enum so
validation and codegen enforce the choices: update the userType property in the
OpenAPI schema (the userType model/property) to include an enum array
[1,2,3,4,5] (and optionally add a nullable/format/type: integer and a brief enum
description or x-enum-descriptions mapping) so tools will validate the allowed
values instead of relying on the prose-only description.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@seerr-api.yml`:
- Around line 3985-3987: The pin property is defined as a free-form string but
should be constrained to exactly four digits; update both occurrences of the pin
schema in seerr-api.yml (the request body schemas that currently show "pin:
type: string description: Optional 4-digit profile PIN") to enforce a 4-digit
format by adding validations such as pattern: '^[0-9]{4}$' (and optionally
minLength: 4 and maxLength: 4) so the schema only accepts exactly four numeric
digits.

In `@src/components/Login/PlexProfileSelector.tsx`:
- Around line 152-160: The Image src may be an empty string for some profiles
which can cause Next.js Image to error or render a broken image; in
PlexProfileSelector (where profile is used and the Image component is rendered)
guard against empty profile.thumb by providing a fallback source or skipping the
Image when profile.thumb is falsy—e.g., use profile.thumb ||
fallbackThumbnailUrl or render a plain placeholder element (same dimensions and
classes) when profile.thumb is empty so Image only receives a valid non-empty
src.
- Around line 50-60: The call to onProfileSelected(profile.id) is not awaited so
async errors slip past the try/catch and setIsSubmitting(false) runs too early;
make the containing handler function asynchronous (add async) and await
onProfileSelected(profile.id) inside the existing try block, leaving the catch
to call setError(intl.formatMessage(messages.selectProfileError)) and the
finally to call setIsSubmitting(false) so loading and error handling behave
correctly for async failures.

---

Outside diff comments:
In `@server/routes/auth.ts`:
- Around line 319-333: The logger.info message is misleading—inside the branch
that returns next({ status: 403, error: ApiErrorCode.NewPlexLoginDisabled, ...
}) it incorrectly says "creating new Jellyseerr user"; update the log text in
the logger.info call (near the same block using logger.info, req.ip,
account.email, account.id, account.username and the subsequent next(...) call)
to accurately reflect that the Plex sign-in was denied due to lack of media
server access (e.g., "Sign-in attempt from Plex user without access to the media
server; denying access").

---

Duplicate comments:
In `@server/api/plextv.ts`:
- Around line 376-384: The axios.post call in switchProfile (the POST to urlPath
with pin) lacks a timeout and can hang; update the request options passed to
axios.post in the switchProfile method to include a timeout (e.g. timeout:
10000) alongside baseURL and headers so it matches getProfiles/getUsers behavior
and fails fast if the Plex API is unresponsive.

In `@server/routes/auth.ts`:
- Around line 649-668: The current branch that handles an existing user with a
different plexId and userType !== UserType.PLEX_PROFILE (symbols: profileUser,
mainUser, UserType.PLEX_PROFILE, logger.warn, req.session.userId, profileId)
should not silently authenticate; instead, stop setting req.session.userId and
return a failure response (e.g., 403 Forbidden or 400) with a clear error
message and relevant identifiers in the payload after logging the warning.
Update the handler so when the condition matches you log the incident via
logger.warn and then immediately return res.status(...).json({ error: 'Profile
selection conflicts with an existing non-profile user', profileId, userId:
profileUser.id }) without mutating session or proceeding to authenticate.

---

Nitpick comments:
In `@seerr-api.yml`:
- Line 117: The userType schema currently only documents allowed numeric values
in its description; change it into an explicit enum so validation and codegen
enforce the choices: update the userType property in the OpenAPI schema (the
userType model/property) to include an enum array [1,2,3,4,5] (and optionally
add a nullable/format/type: integer and a brief enum description or
x-enum-descriptions mapping) so tools will validate the allowed values instead
of relying on the prose-only description.

In `@server/api/plextv.ts`:
- Around line 396-410: The try/catch in validateProfilePin is dead because
switchProfile already catches errors and returns false; remove the try/catch and
simply return the result of this.switchProfile(profileId, pin) in
validateProfilePin (leave switchProfile unchanged), and delete the unreachable
logger.error block so validateProfilePin becomes a thin wrapper that returns
await this.switchProfile(profileId, pin).

In `@server/routes/auth.ts`:
- Around line 497-505: Remove the redundant plextv.getUser() call and its
surrounding try/catch in the route handler—if validateProfilePin(...) already
returned true and profile switching succeeded, do not make an extra
plextv.getUser() call; delete the block that calls plextv.getUser() and returns
a 401 on catch, leaving the flow to continue after validateProfilePin succeeds
(refer to validateProfilePin and plextv.getUser in auth.ts to locate the code).
- Around line 604-619: Remove the redundant second lookup and its handling:
delete the const exactProfileUser = await userRepository.findOne({ where: {
plexProfileId: profileId } }); block and the subsequent if (exactProfileUser) {
... } return; since profileUser was already queried by plexProfileId earlier,
this code is unreachable; if you intended to reuse a found user ensure you
reference profileUser (and set req.session.userId = profileUser.id) instead.

In `@src/components/Login/index.tsx`:
- Around line 145-161: The code repeatedly calls profiles.find((p) => p.id ===
profileId) for the same profileId; cache the found profile in a local variable
(e.g., const profile = profiles.find(p => p.id === profileId)) before the block
and then replace the multiple finds used by setShowPinEntry, setPinProfileId,
setPinProfileName, setPinProfileThumb, setPinIsProtected, and setPinIsMainUser
with properties read from that cached profile (with the same fallback values),
ensuring you handle the case where profile is undefined.

In `@src/components/UserList/PlexImportModal.tsx`:
- Around line 159-188: Define an explicit TypeScript interface (e.g.,
PlexImportResponse { skipped?: { type: string }[]; ... }) for the API payload
returned in PlexImportModal and apply it to the axios call (use
axios.post<PlexImportResponse>(...) or type response.data as PlexImportResponse)
so that response.skipped is strongly typed; update any local usages
(response.skipped, skippedUsers, skippedProfiles) to rely on the typed response
and keep existing optional checks (e.g., response.skipped && ...) to preserve
runtime safety.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73688ba and a2a303b.

📒 Files selected for processing (9)
  • seerr-api.yml
  • server/api/plextv.ts
  • server/routes/auth.ts
  • server/routes/settings/index.ts
  • src/components/Login/PlexPinEntry.tsx
  • src/components/Login/PlexProfileSelector.tsx
  • src/components/Login/index.tsx
  • src/components/UserList/PlexImportModal.tsx
  • src/i18n/locale/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Login/PlexPinEntry.tsx

Comment thread seerr-api.yml
Comment thread src/components/Login/PlexProfileSelector.tsx
Comment thread src/components/Login/PlexProfileSelector.tsx
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

♻️ Duplicate comments (2)
server/api/plextv.ts (1)

376-384: ⚠️ Potential issue | 🟠 Major

Add a timeout to switchProfile external call.

This direct Plex request has no timeout; stalled upstream networking can hang the request path and cascade into auth latency/failures.

🔧 Suggested fix
       const response = await axios.post(urlPath, pin ? { pin } : {}, {
         baseURL: 'https://clients.plex.tv',
+        timeout: 10000,
         headers: {
           Accept: 'application/json',
           'Content-Type': 'application/json',
           'X-Plex-Token': this.authToken,
           'X-Plex-Client-Identifier': randomUUID(),
         },
       });
#!/bin/bash
# Verify whether switchProfile axios config includes a timeout.
rg -n -A12 -B2 "axios\.post\(urlPath" server/api/plextv.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/plextv.ts` around lines 376 - 384, The axios.post call inside
switchProfile is missing a request timeout which can hang; update the
axios.post(urlPath, pin ? { pin } : {}, { baseURL: 'https://clients.plex.tv',
headers: { ... } }) call to include a timeout property (e.g. timeout: 5000 or
use an existing config value like this.requestTimeout) in the options object so
the external Plex request fails fast on network stalls; ensure the timeout is
applied to the same options object passed to axios.post in the switchProfile
implementation.
server/routes/auth.ts (1)

140-167: ⚠️ Potential issue | 🔴 Critical

/auth/plex direct profile login bypasses profile PIN flow.

This path creates a session from profileId without enforcing PIN checks for protected profiles. Profile authentication should be centralized in /auth/plex/profile/select.

🔒 Suggested hardening
-    // Handle direct profile login
-    if (body.profileId) {
-      const profileUser = await userRepository.findOne({
-        where: { plexProfileId: body.profileId },
-      });
-
-      if (!profileUser) {
-        return next({
-          status: 400,
-          message: 'Invalid profile selection.',
-        });
-      }
-      if (!profiles.some((p) => p.id === body.profileId)) {
-        return next({
-          status: 403,
-          error: 'Auth token does not have access to this profile.',
-        });
-      }
-
-      profileUser.plexToken = body.authToken;
-      await userRepository.save(profileUser);
-
-      if (req.session) {
-        req.session.userId = profileUser.id;
-      }
-
-      return res.status(200).json(profileUser.filter() ?? {});
-    }
+    // Force all profile authentication through /auth/plex/profile/select
+    if (body.profileId) {
+      return next({
+        status: 400,
+        message: 'Use /auth/plex/profile/select for profile authentication.',
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/auth.ts` around lines 140 - 167, The current /auth/plex handler
directly creates a session and sets profileUser.plexToken when body.profileId is
present, bypassing PIN checks; instead, do not perform session creation or token
save in this branch—either remove this direct-profile block or delegate to the
centralized profile auth flow (/auth/plex/profile/select) so PIN-protected
profiles are validated before calling userRepository.save or setting
req.session.userId; ensure you reuse the same profile selection/PIN verification
logic that checks profiles.some((p) => p.id === body.profileId) and enforces PIN
rules prior to updating profileUser or returning the filtered profile.
🤖 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/api/plextv.ts`:
- Around line 273-274: getProfiles() currently returns a synthetic main profile
with protected: false when Plex Home profile retrieval fails, which weakens auth
gating; change getProfiles() to fail closed by either throwing/propagating the
retrieval error or returning a failure result instead of the synthetic
unprotected profile (do not leave the profile object with protected: false).
Locate the code paths that construct the fallback profile (the object with
protected: false) in getProfiles() and in the similar blocks referenced later,
remove that permissive fallback, and ensure callers handle the propagated error
or explicit failure so PIN/profile-selection safeguards remain enforced.

In `@server/routes/auth.ts`:
- Around line 126-138: The current check only validates a PIN when body.pin
exists, allowing protected main profiles (mainUserProfile?.protected) to
authenticate with no PIN; update the logic in the block that uses
mainUserProfile and plextv.validateProfilePin so that if (!body.profileId &&
mainUserProfile?.protected) you first require a PIN (if no body.pin return
next({status:403, error:'PIN_REQUIRED.'})), then call
plextv.validateProfilePin(mainUserProfile.id, body.pin) and reject on invalid
PIN as before; apply the same change to the duplicate block around the 370-384
area to ensure protected main profiles cannot proceed without a PIN.

---

Duplicate comments:
In `@server/api/plextv.ts`:
- Around line 376-384: The axios.post call inside switchProfile is missing a
request timeout which can hang; update the axios.post(urlPath, pin ? { pin } :
{}, { baseURL: 'https://clients.plex.tv', headers: { ... } }) call to include a
timeout property (e.g. timeout: 5000 or use an existing config value like
this.requestTimeout) in the options object so the external Plex request fails
fast on network stalls; ensure the timeout is applied to the same options object
passed to axios.post in the switchProfile implementation.

In `@server/routes/auth.ts`:
- Around line 140-167: The current /auth/plex handler directly creates a session
and sets profileUser.plexToken when body.profileId is present, bypassing PIN
checks; instead, do not perform session creation or token save in this
branch—either remove this direct-profile block or delegate to the centralized
profile auth flow (/auth/plex/profile/select) so PIN-protected profiles are
validated before calling userRepository.save or setting req.session.userId;
ensure you reuse the same profile selection/PIN verification logic that checks
profiles.some((p) => p.id === body.profileId) and enforces PIN rules prior to
updating profileUser or returning the filtered profile.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2a303b and 7f277ae.

📒 Files selected for processing (6)
  • seerr-api.yml
  • server/api/plextv.ts
  • server/routes/auth.ts
  • src/components/Login/PlexProfileSelector.tsx
  • src/components/Login/index.tsx
  • src/components/UserList/PlexImportModal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/Login/PlexProfileSelector.tsx

Comment thread server/api/plextv.ts Outdated
Comment thread server/routes/auth.ts Outdated
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Mar 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 3, 2026

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Cannot merge due to merge conflicts preview PRs deployed for testing with tag `:preview-prxx`

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add plex home linking

5 participants