Skip to content

Commit

Permalink
feat(linked-accounts): support linking/unlinking emby accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelhthomas committed Oct 18, 2024
1 parent 2b6c8c3 commit 4a32662
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 34 deletions.
35 changes: 27 additions & 8 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,14 @@ userSettingsRoutes.delete<{ id: string }>(
'/linked-accounts/plex',
isOwnProfileOrAdmin(),
async (req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);

// Make sure Plex login is enabled
if (settings.main.mediaServerType !== MediaServerType.PLEX) {
return res.status(500).json({ error: 'Plex login is disabled' });
}

try {
const user = await userRepository.findOne({
where: { id: Number(req.params.id) },
Expand Down Expand Up @@ -393,8 +399,11 @@ userSettingsRoutes.post<{ username: string; password: string }>(
return next({ status: 401, message: 'Unauthorized' });
}
// Make sure jellyfin login is enabled
if (settings.main.mediaServerType !== MediaServerType.JELLYFIN) {
return res.status(500).json({ error: 'Jellyfin login is disabled' });
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY
) {
return res.status(500).json({ error: 'Jellyfin/Emby login is disabled' });
}

// Do not allow linking of an already linked account
Expand All @@ -404,8 +413,7 @@ userSettingsRoutes.post<{ username: string; password: string }>(
})
) {
return res.status(422).json({
error:
'The specified Jellyfin account is already linked to a Jellyseerr user',
error: 'The specified account is already linked to a Jellyseerr user',
});
}

Expand Down Expand Up @@ -440,15 +448,17 @@ userSettingsRoutes.post<{ username: string; password: string }>(
})
) {
return res.status(422).json({
error:
'The specified Jellyfin account is already linked to a Jellyseerr user',
error: 'The specified account is already linked to a Jellyseerr user',
});
}

const user = req.user;

// valid jellyfin user found, link to current user
user.userType = UserType.JELLYFIN;
user.userType =
settings.main.mediaServerType === MediaServerType.EMBY
? UserType.EMBY
: UserType.JELLYFIN;
user.jellyfinUserId = account.User.Id;
user.jellyfinUsername = account.User.Name;
user.jellyfinAuthToken = account.AccessToken;
Expand All @@ -457,7 +467,7 @@ userSettingsRoutes.post<{ username: string; password: string }>(

return res.status(204).send();
} catch (e) {
logger.error('Failed to link Jellyfin account to user.', {
logger.error('Failed to link account to user.', {
label: 'API',
ip: req.ip,
error: e,
Expand All @@ -478,8 +488,17 @@ userSettingsRoutes.delete<{ id: string }>(
'/linked-accounts/jellyfin',
isOwnProfileOrAdmin(),
async (req, res, next) => {
const settings = getSettings();
const userRepository = getRepository(User);

// Make sure jellyfin login is enabled
if (
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.main.mediaServerType !== MediaServerType.EMBY
) {
return res.status(500).json({ error: 'Jellyfin/Emby login is disabled' });
}

try {
const user = await userRepository.findOne({
where: { id: Number(req.params.id) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useUser } from '@app/hooks/useUser';
import { RequestError } from '@app/types/error';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { MediaServerType } from '@server/constants/server';
import { Field, Form, Formik } from 'formik';
import { useState } from 'react';
import { useIntl } from 'react-intl';
Expand All @@ -13,17 +14,18 @@ import * as Yup from 'yup';
const messages = defineMessages(
'components.UserProfile.UserSettings.LinkJellyfinModal',
{
title: 'Link Jellyfin Account',
title: 'Link {mediaServerName} Account',
description:
'Enter your Jellyfin credentials to link your account with Jellyseerr.',
'Enter your {mediaServerName} credentials to link your account with {applicationName}.',
username: 'Username',
password: 'Password',
usernameRequired: 'You must provide a username',
passwordRequired: 'You must provide a password',
saving: 'Adding…',
save: 'Link',
errorUnauthorized: 'Unable to connect to Jellyfin using your credentials',
errorExists: 'This account is already linked to a Jellyseerr user',
errorUnauthorized:
'Unable to connect to {mediaServerName} using your credentials',
errorExists: 'This account is already linked to a {applicationName} user',
errorUnknown: 'An unknown error occurred',
}
);
Expand Down Expand Up @@ -53,6 +55,12 @@ const LinkJellyfinModal: React.FC<LinkJellyfinModalProps> = ({
),
});

const applicationName = settings.currentSettings.applicationTitle;
const mediaServerName =
settings.currentSettings.mediaServerType === MediaServerType.EMBY
? 'Emby'
: 'Jellyfin';

return (
<Transition
appear
Expand Down Expand Up @@ -91,11 +99,17 @@ const LinkJellyfinModal: React.FC<LinkJellyfinModalProps> = ({
onSave();
} catch (e) {
if (e instanceof RequestError && e.status == 401) {
setError(intl.formatMessage(messages.errorUnauthorized));
setError(
intl.formatMessage(messages.errorUnauthorized, {
mediaServerName,
})
);
} else if (e instanceof RequestError && e.status == 422) {
setError(intl.formatMessage(messages.errorExists));
setError(
intl.formatMessage(messages.errorExists, { applicationName })
);
} else {
setError(intl.formatMessage(messages.errorServer));
setError(intl.formatMessage(messages.errorUnknown));
}
}
}}
Expand All @@ -116,12 +130,13 @@ const LinkJellyfinModal: React.FC<LinkJellyfinModalProps> = ({
}
okDisabled={isSubmitting || !isValid}
onOk={() => handleSubmit()}
title={intl.formatMessage(messages.title)}
title={intl.formatMessage(messages.title, { mediaServerName })}
dialogClass="sm:max-w-lg"
>
<Form id="link-jellyfin-account">
{intl.formatMessage(messages.description, {
applicationName: settings.currentSettings.applicationTitle,
mediaServerName,
applicationName,
})}
{error && (
<div className="mt-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import EmbyLogo from '@app/assets/services/emby-icon-only.svg';
import JellyfinLogo from '@app/assets/services/jellyfin-icon.svg';
import PlexLogo from '@app/assets/services/plex.svg';
import Alert from '@app/components/Common/Alert';
import ConfirmButton from '@app/components/Common/ConfirmButton';
import Dropdown from '@app/components/Common/Dropdown';
import PageTitle from '@app/components/Common/PageTitle';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import { Permission, UserType, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { RequestError } from '@app/types/error';
import defineMessages from '@app/utils/defineMessages';
import PlexOAuth from '@app/utils/plex';
import { TrashIcon } from '@heroicons/react/24/solid';
import { MediaServerType } from '@server/constants/server';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import useSWR from 'swr';
import LinkJellyfinModal from './LinkJellyfinModal';
Expand All @@ -38,8 +39,9 @@ const messages = defineMessages(
const plexOAuth = new PlexOAuth();

const enum LinkedAccountType {
Plex,
Jellyfin,
Plex = 'Plex',
Jellyfin = 'Jellyfin',
Emby = 'Emby',
}

type LinkedAccount = {
Expand All @@ -63,14 +65,26 @@ const UserLinkedAccountsSettings = () => {
const [showJellyfinModal, setShowJellyfinModal] = useState(false);
const [error, setError] = useState<string | null>(null);

const accounts: LinkedAccount[] = [
...(user?.plexUsername
? [{ type: LinkedAccountType.Plex, username: user?.plexUsername }]
: []),
...(user?.jellyfinUsername
? [{ type: LinkedAccountType.Jellyfin, username: user?.jellyfinUsername }]
: []),
];
const accounts: LinkedAccount[] = useMemo(() => {
const accounts: LinkedAccount[] = [];
if (!user) return accounts;
if (user.userType === UserType.PLEX && user.plexUsername)
accounts.push({
type: LinkedAccountType.Plex,
username: user.plexUsername,
});
if (user.userType === UserType.EMBY && user.jellyfinUsername)
accounts.push({
type: LinkedAccountType.Emby,
username: user.jellyfinUsername,
});
if (user.userType === UserType.JELLYFIN && user.jellyfinUsername)
accounts.push({
type: LinkedAccountType.Emby,
username: user.jellyfinUsername,
});
return accounts;
}, [user]);

const linkPlexAccount = async () => {
setError(null);
Expand Down Expand Up @@ -117,6 +131,13 @@ const UserLinkedAccountsSettings = () => {
settings.currentSettings.mediaServerType != MediaServerType.JELLYFIN ||
accounts.some((a) => a.type == LinkedAccountType.Jellyfin),
},
{
name: 'Emby',
action: () => setShowJellyfinModal(true),
hide:
settings.currentSettings.mediaServerType != MediaServerType.EMBY ||
accounts.some((a) => a.type == LinkedAccountType.Emby),
},
].filter((l) => !l.hide);

const deleteRequest = async (account: string) => {
Expand Down Expand Up @@ -198,13 +219,15 @@ const UserLinkedAccountsSettings = () => {
<div className="flex aspect-square h-full items-center justify-center rounded-full bg-neutral-800">
<PlexLogo className="w-9" />
</div>
) : acct.type == LinkedAccountType.Emby ? (
<EmbyLogo />
) : (
<JellyfinLogo />
)}
</div>
<div>
<div className="truncate text-sm font-bold text-gray-300">
{acct.type == LinkedAccountType.Plex ? 'Plex' : 'Jellyfin'}
{acct.type}
</div>
<div className="text-xl font-semibold text-white">
{acct.username}
Expand Down
8 changes: 4 additions & 4 deletions src/i18n/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1205,15 +1205,15 @@
"components.UserProfile.ProfileHeader.profile": "View Profile",
"components.UserProfile.ProfileHeader.settings": "Edit Settings",
"components.UserProfile.ProfileHeader.userid": "User ID: {userid}",
"components.UserProfile.UserSettings.LinkJellyfinModal.description": "Enter your Jellyfin credentials to link your account with Jellyseerr.",
"components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "This account is already linked to a Jellyseerr user",
"components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Unable to connect to Jellyfin using your credentials",
"components.UserProfile.UserSettings.LinkJellyfinModal.description": "Enter your {mediaServerName} credentials to link your account with {applicationName}.",
"components.UserProfile.UserSettings.LinkJellyfinModal.errorExists": "This account is already linked to a {applicationName} user",
"components.UserProfile.UserSettings.LinkJellyfinModal.errorUnauthorized": "Unable to connect to {mediaServerName} using your credentials",
"components.UserProfile.UserSettings.LinkJellyfinModal.errorUnknown": "An unknown error occurred",
"components.UserProfile.UserSettings.LinkJellyfinModal.password": "Password",
"components.UserProfile.UserSettings.LinkJellyfinModal.passwordRequired": "You must provide a password",
"components.UserProfile.UserSettings.LinkJellyfinModal.save": "Link",
"components.UserProfile.UserSettings.LinkJellyfinModal.saving": "Adding…",
"components.UserProfile.UserSettings.LinkJellyfinModal.title": "Link Jellyfin Account",
"components.UserProfile.UserSettings.LinkJellyfinModal.title": "Link {mediaServerName} Account",
"components.UserProfile.UserSettings.LinkJellyfinModal.username": "Username",
"components.UserProfile.UserSettings.LinkJellyfinModal.usernameRequired": "You must provide a username",
"components.UserProfile.UserSettings.UserGeneralSettings.accounttype": "Account Type",
Expand Down

0 comments on commit 4a32662

Please sign in to comment.