feat(auth): add handling for existing profile users during Plex login
This commit is contained in:
@@ -11,4 +11,5 @@ export enum ApiErrorCode {
|
||||
Unknown = 'UNKNOWN',
|
||||
InvalidPin = 'INVALID_PIN',
|
||||
NewPlexLoginDisabled = 'NEW_PLEX_LOGIN_DISABLED',
|
||||
ProfileUserExists = 'PROFILE_USER_EXISTS',
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user