Skip to content

Commit

Permalink
fix(linked-accounts): probibit unlinking accounts in certain cases
Browse files Browse the repository at this point in the history
Prevents the primary administrator from unlinking their media server account (which would break
sync). Additionally, prevents users without a configured local email and password from unlinking
their accounts, which would render them unable to log in.
  • Loading branch information
michaelhthomas committed Oct 18, 2024
1 parent 97c3935 commit 2b6c8c3
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 16 deletions.
44 changes: 44 additions & 0 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,28 @@ userSettingsRoutes.delete<{ id: string }>(
return next({ status: 404, message: 'User not found.' });
}

if (user.id === 1) {
return next({
status: 400,
message:
'Cannot unlink media server accounts for the primary administrator.',
});
}

const hasPassword = !!(
await userRepository.findOne({
where: { id: user.id },
select: ['id', 'password'],
})
)?.password;

if (!user.email || !hasPassword) {
return next({
status: 400,
message: 'User does not have a local email or password set.',
});
}

user.userType = UserType.LOCAL;
user.plexId = null;
user.plexUsername = null;
Expand Down Expand Up @@ -467,6 +489,28 @@ userSettingsRoutes.delete<{ id: string }>(
return next({ status: 404, message: 'User not found.' });
}

if (user.id === 1) {
return next({
status: 400,
message:
'Cannot unlink media server accounts for the primary administrator.',
});
}

const hasPassword = !!(
await userRepository.findOne({
where: { id: user.id },
select: ['id', 'password'],
})
)?.password;

if (!user.email || !hasPassword) {
return next({
status: 400,
message: 'User does not have a local email or password set.',
});
}

user.userType = UserType.LOCAL;
user.jellyfinUserId = null;
user.jellyfinUsername = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { MediaServerType } from '@server/constants/server';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
import LinkJellyfinModal from './LinkJellyfinModal';

const messages = defineMessages(
Expand Down Expand Up @@ -56,6 +57,9 @@ const UserLinkedAccountsSettings = () => {
hasPermission,
revalidate: revalidateUser,
} = useUser({ id: Number(router.query.userId) });
const { data: passwordInfo } = useSWR<{ hasPassword: boolean }>(
user ? `/api/v1/user/${user?.id}/settings/password` : null
);
const [showJellyfinModal, setShowJellyfinModal] = useState(false);
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -149,6 +153,8 @@ const UserLinkedAccountsSettings = () => {
);
}

const enableMediaServerUnlink = user?.id !== 1 && passwordInfo?.hasPassword;

return (
<>
<PageTitle
Expand Down Expand Up @@ -179,11 +185,7 @@ const UserLinkedAccountsSettings = () => {
</div>
)}
</div>
{error && (
<Alert title={intl.formatMessage(globalMessages.failed)} type="error">
{error}
</Alert>
)}
{error && <Alert title={error} type="error" />}
{accounts.length ? (
<ul className="space-y-4">
{accounts.map((acct, i) => (
Expand All @@ -209,17 +211,19 @@ const UserLinkedAccountsSettings = () => {
</div>
</div>
<div className="flex-grow" />
<ConfirmButton
onClick={() => {
deleteRequest(
acct.type == LinkedAccountType.Plex ? 'plex' : 'jellyfin'
);
}}
confirmText={intl.formatMessage(globalMessages.areyousure)}
>
<TrashIcon />
<span>{intl.formatMessage(globalMessages.delete)}</span>
</ConfirmButton>
{enableMediaServerUnlink && (
<ConfirmButton
onClick={() => {
deleteRequest(
acct.type == LinkedAccountType.Plex ? 'plex' : 'jellyfin'
);
}}
confirmText={intl.formatMessage(globalMessages.areyousure)}
>
<TrashIcon />
<span>{intl.formatMessage(globalMessages.delete)}</span>
</ConfirmButton>
)}
</li>
))}
</ul>
Expand Down

0 comments on commit 2b6c8c3

Please sign in to comment.