feat(auth): add handling for existing profile users during Plex login

This commit is contained in:
0xsysr3ll
2025-07-22 16:42:43 +02:00
parent 718c64f973
commit c6f98a84d4
5 changed files with 72 additions and 8 deletions

View File

@@ -11,4 +11,5 @@ export enum ApiErrorCode {
Unknown = 'UNKNOWN',
InvalidPin = 'INVALID_PIN',
NewPlexLoginDisabled = 'NEW_PLEX_LOGIN_DISABLED',
ProfileUserExists = 'PROFILE_USER_EXISTS',
}

View File

@@ -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({

View File

@@ -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,

View File

@@ -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);

View File

@@ -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",