feat(linked-accounts): support linking/unlinking emby accounts
This commit is contained in:
@@ -358,8 +358,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) },
|
||||
@@ -415,8 +421,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
|
||||
@@ -426,8 +435,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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,15 +470,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;
|
||||
@@ -479,7 +489,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,
|
||||
@@ -500,8 +510,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) },
|
||||
|
||||
@@ -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';
|
||||
@@ -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',
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -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">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -5,7 +6,7 @@ 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';
|
||||
@@ -13,7 +14,7 @@ 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';
|
||||
@@ -38,8 +39,9 @@ const messages = defineMessages(
|
||||
const plexOAuth = new PlexOAuth();
|
||||
|
||||
const enum LinkedAccountType {
|
||||
Plex,
|
||||
Jellyfin,
|
||||
Plex = 'Plex',
|
||||
Jellyfin = 'Jellyfin',
|
||||
Emby = 'Emby',
|
||||
}
|
||||
|
||||
type LinkedAccount = {
|
||||
@@ -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);
|
||||
@@ -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) => {
|
||||
@@ -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}
|
||||
|
||||
@@ -1261,15 +1261,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",
|
||||
|
||||
Reference in New Issue
Block a user