From c6f98a84d4a6163944454afdf0f856688b7a0851 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <0xsysr3ll@pm.me> Date: Tue, 22 Jul 2025 16:42:43 +0200 Subject: [PATCH] feat(auth): add handling for existing profile users during Plex login --- server/constants/error.ts | 1 + server/routes/auth.ts | 32 ++++++++++++++++++++++++++ server/routes/user/index.ts | 41 +++++++++++++++++++++++++++------- src/components/Login/index.tsx | 5 +++++ src/i18n/locale/en.json | 1 + 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/server/constants/error.ts b/server/constants/error.ts index ce68f8df..98b07b5a 100644 --- a/server/constants/error.ts +++ b/server/constants/error.ts @@ -11,4 +11,5 @@ export enum ApiErrorCode { Unknown = 'UNKNOWN', InvalidPin = 'INVALID_PIN', NewPlexLoginDisabled = 'NEW_PLEX_LOGIN_DISABLED', + ProfileUserExists = 'PROFILE_USER_EXISTS', } diff --git a/server/routes/auth.ts b/server/routes/auth.ts index d8e424f6..49cb39ef 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -168,6 +168,38 @@ authRoutes.post('/plex', async (req, res, next) => { }) .getOne(); + const safeUsername = (account.username || account.title) + .replace(/\s+/g, '.') + .replace(/[^a-zA-Z0-9._-]/g, ''); + const emailPrefix = account.email.split('@')[0]; + const domainPart = account.email.includes('@') + ? account.email.split('@')[1] + : 'plex.local'; + const proposedEmail = `${emailPrefix}+${safeUsername}@${domainPart}`; + const existingProfileUser = await userRepository.findOne({ + where: [ + { plexUsername: account.username, isPlexProfile: true }, + { email: proposedEmail, isPlexProfile: true }, + ], + }); + if (!user && existingProfileUser) { + logger.warn( + 'Main user login attempted but profile user already exists for this person', + { + label: 'Auth', + plexUsername: account.username, + email: account.email, + profileUserId: existingProfileUser.id, + } + ); + return next({ + status: 409, + message: + 'A profile user already exists for this Plex account. Please contact your administrator to resolve this duplicate.', + error: ApiErrorCode.ProfileUserExists, + }); + } + if (!user && !(await userRepository.count())) { // First user setup through standard auth flow user = new User({ diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index ead79a46..d5688c97 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -621,26 +621,37 @@ router.post( if (profileIds && profileIds.length > 0) { const profiles = await mainPlexTv.getProfiles(); + // Filter out real Plex users (with email/isMainUser) from importable profiles + const importableProfiles = profiles.filter((p: any) => !p.isMainUser); for (const profileId of profileIds) { - const profileData = profiles.find((p: any) => p.id === profileId); + const profileData = importableProfiles.find( + (p: any) => p.id === profileId + ); if (profileData) { + // Check for existing user with same plexProfileId + const existingUser = await userRepository.findOne({ + where: { plexProfileId: profileId }, + }); + const emailPrefix = mainUser.email.split('@')[0]; const domainPart = mainUser.email.includes('@') ? mainUser.email.split('@')[1] : 'plex.local'; - const safeUsername = (profileData.username || profileData.title) .replace(/\s+/g, '.') .replace(/[^a-zA-Z0-9._-]/g, ''); + const proposedEmail = `${emailPrefix}+${safeUsername}@${domainPart}`; - // Check for existing user with same username or email - const existingUser = await userRepository.findOne({ + // Check for main user with same plexUsername or email + const mainUserDuplicate = await userRepository.findOne({ where: [ - { plexUsername: profileData.username || profileData.title }, - { email: `${emailPrefix}+${safeUsername}@${domainPart}` }, - { plexProfileId: profileId }, + { + plexUsername: profileData.username || profileData.title, + isPlexProfile: false, + }, + { email: proposedEmail, isPlexProfile: false }, ], }); @@ -654,8 +665,22 @@ router.post( continue; } + if (mainUserDuplicate) { + // Skip this profile and add to skipped list, but ensure main user is imported + skippedItems.push({ + id: profileId, + type: 'profile', + reason: 'MAIN_USER_ALREADY_EXISTS', + }); + // If main user is not already in createdUsers, add it + if (!createdUsers.find((u) => u.id === mainUserDuplicate.id)) { + createdUsers.push(mainUserDuplicate); + } + continue; + } + const profileUser = new User({ - email: `${emailPrefix}+${safeUsername}@${domainPart}`, + email: proposedEmail, plexUsername: profileData.username || profileData.title, plexId: mainUser.plexId, plexToken: mainUser.plexToken, diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index e806c747..dee57f63 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -36,6 +36,8 @@ const messages = defineMessages('components.Login', { authFailed: 'Authentication failed', invalidPin: 'Invalid PIN. Please try again.', accessDenied: 'Access denied.', + profileUserExists: + 'A profile user already exists for this Plex account. Please contact your administrator to resolve this duplicate.', }); const Login = () => { @@ -177,6 +179,9 @@ const Login = () => { case ApiErrorCode.InvalidPin: msg = intl.formatMessage(messages.invalidPin); break; + case ApiErrorCode.ProfileUserExists: + msg = intl.formatMessage(messages.profileUserExists); + break; default: if (httpStatus === 401) { msg = intl.formatMessage(messages.invalidPin); diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 76fb884e..9be26c01 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -269,6 +269,7 @@ "components.Login.orsigninwith": "Or sign in with", "components.Login.password": "Password", "components.Login.port": "Port", + "components.Login.profileUserExists": "A profile user already exists for this Plex account. Please contact your administrator to resolve this duplicate.", "components.Login.save": "Add", "components.Login.saving": "Adding…", "components.Login.servertype": "Server Type",