From d60413ca935cabba1e7badcbf19789ca33ae0c1b Mon Sep 17 00:00:00 2001 From: Michael Thomas Date: Sat, 18 Jan 2025 14:18:42 -0500 Subject: [PATCH 1/3] feat: support disabling jellyfin login --- docs/using-jellyseerr/settings/users.md | 8 ++ server/interfaces/api/settingsInterfaces.ts | 1 + server/lib/settings/index.ts | 4 + server/routes/auth.ts | 14 ++- .../Common/LabeledCheckbox/index.tsx | 43 +++++++ .../Settings/SettingsUsers/index.tsx | 105 ++++++++++++++---- src/context/SettingsContext.tsx | 1 + src/i18n/locale/en.json | 7 +- src/pages/_app.tsx | 1 + 9 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 src/components/Common/LabeledCheckbox/index.tsx diff --git a/docs/using-jellyseerr/settings/users.md b/docs/using-jellyseerr/settings/users.md index ebe547efc..0fdeb7db3 100644 --- a/docs/using-jellyseerr/settings/users.md +++ b/docs/using-jellyseerr/settings/users.md @@ -14,6 +14,14 @@ When disabled, your mediaserver OAuth becomes the only sign-in option, and any " This setting is **enabled** by default. +## Enable Jellyfin/Emby/Plex Sign-In + +When enabled, users will be able to sign in to Jellyseerr using their Jellyfin/Emby/Plex credentials, provided they have linked their media server accounts. + +When disabled, users will only be able to sign in using their email address. Users without a password set will not be able to sign in to Jellyseerr. + +This setting is **enabled** by default. + ## Enable New Jellyfin/Emby/Plex Sign-In When enabled, users with access to your media server will be able to sign in to Jellyseerr even if they have not yet been imported. Users will be automatically assigned the permissions configured in the [Default Permissions](#default-permissions) setting upon first sign-in. diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 017eef856..0e97c2bf4 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -30,6 +30,7 @@ export interface PublicSettingsResponse { applicationUrl: string; hideAvailable: boolean; localLogin: boolean; + mediaServerLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; discoverRegion: string; diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 343c01e2f..d29f329ea 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -124,6 +124,7 @@ export interface MainSettings { }; hideAvailable: boolean; localLogin: boolean; + mediaServerLogin: boolean; newPlexLogin: boolean; discoverRegion: string; streamingRegion: string; @@ -147,6 +148,7 @@ interface FullPublicSettings extends PublicSettings { applicationUrl: string; hideAvailable: boolean; localLogin: boolean; + mediaServerLogin: boolean; movie4kEnabled: boolean; series4kEnabled: boolean; discoverRegion: string; @@ -340,6 +342,7 @@ class Settings { }, hideAvailable: false, localLogin: true, + mediaServerLogin: true, newPlexLogin: true, discoverRegion: '', streamingRegion: '', @@ -582,6 +585,7 @@ class Settings { applicationUrl: this.data.main.applicationUrl, hideAvailable: this.data.main.hideAvailable, localLogin: this.data.main.localLogin, + mediaServerLogin: this.data.main.mediaServerLogin, jellyfinForgotPasswordUrl: this.data.jellyfin.jellyfinForgotPasswordUrl, movie4kEnabled: this.data.radarr.some( (radarr) => radarr.is4k && radarr.isDefault diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 5fe0174ee..b3e4f4494 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -56,8 +56,9 @@ authRoutes.post('/plex', async (req, res, next) => { } if ( - settings.main.mediaServerType != MediaServerType.PLEX && - settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED + settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED && + (settings.main.mediaServerLogin === false || + settings.main.mediaServerType != MediaServerType.PLEX) ) { return res.status(500).json({ error: 'Plex login is disabled' }); } @@ -231,10 +232,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => { //Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured if ( - settings.main.mediaServerType !== MediaServerType.JELLYFIN && - settings.main.mediaServerType !== MediaServerType.EMBY && + // media server not configured, allow login for setup settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED && - settings.jellyfin.ip !== '' + (settings.main.mediaServerLogin === false || + // media server is neither jellyfin or emby + (settings.main.mediaServerType !== MediaServerType.JELLYFIN && + settings.main.mediaServerType !== MediaServerType.EMBY && + settings.jellyfin.ip !== '')) ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); } diff --git a/src/components/Common/LabeledCheckbox/index.tsx b/src/components/Common/LabeledCheckbox/index.tsx new file mode 100644 index 000000000..da949e233 --- /dev/null +++ b/src/components/Common/LabeledCheckbox/index.tsx @@ -0,0 +1,43 @@ +import { Field } from 'formik'; + +interface LabeledCheckboxProps { + id: string; + className?: string; + label: string; + description: string; + onChange: () => void; + children?: React.ReactNode; +} + +const LabeledCheckbox: React.FC = ({ + id, + className, + label, + description, + onChange, + children, +}) => { + return ( + <> +
+
+ +
+
+ +
+
+ { + /* can hold child checkboxes */ + children &&
{children}
+ } + + ); +}; + +export default LabeledCheckbox; diff --git a/src/components/Settings/SettingsUsers/index.tsx b/src/components/Settings/SettingsUsers/index.tsx index 7f6fa1fcf..8203360bd 100644 --- a/src/components/Settings/SettingsUsers/index.tsx +++ b/src/components/Settings/SettingsUsers/index.tsx @@ -1,4 +1,5 @@ import Button from '@app/components/Common/Button'; +import LabeledCheckbox from '@app/components/Common/LabeledCheckbox'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import PageTitle from '@app/components/Common/PageTitle'; import PermissionEdit from '@app/components/PermissionEdit'; @@ -13,6 +14,7 @@ import { Field, Form, Formik } from 'formik'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR, { mutate } from 'swr'; +import * as yup from 'yup'; const messages = defineMessages('components.Settings.SettingsUsers', { users: 'Users', @@ -20,9 +22,15 @@ const messages = defineMessages('components.Settings.SettingsUsers', { userSettingsDescription: 'Configure global and default user settings.', toastSettingsSuccess: 'User settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', + loginMethods: 'Login Methods', + loginMethodsTip: 'Configure login methods for users.', localLogin: 'Enable Local Sign-In', localLoginTip: - 'Allow users to sign in using their email address and password, instead of {mediaServerName} OAuth', + 'Allow users to sign in using their email address and password', + mediaServerLogin: 'Enable {mediaServerName} Sign-In', + mediaServerLoginTip: + 'Allow users to sign in using their {mediaServerName} account', + atLeastOneAuth: 'At least one authentication method must be selected.', newPlexLogin: 'Enable New {mediaServerName} Sign-In', newPlexLoginTip: 'Allow {mediaServerName} users to sign in without first being imported', @@ -42,6 +50,27 @@ const SettingsUsers = () => { } = useSWR('/api/v1/settings/main'); const settings = useSettings(); + const schema = yup + .object() + .shape({ + localLogin: yup.boolean(), + mediaServerLogin: yup.boolean(), + }) + .test({ + name: 'atLeastOneAuth', + test: function (values) { + const isValid = ['localLogin', 'mediaServerLogin'].some( + (field) => !!values[field] + ); + + if (isValid) return true; + return this.createError({ + path: 'localLogin | mediaServerLogin', + message: intl.formatMessage(messages.atLeastOneAuth), + }); + }, + }); + if (!data && !error) { return ; } @@ -52,6 +81,8 @@ const SettingsUsers = () => { ? 'Jellyfin' : settings.currentSettings.mediaServerType === MediaServerType.EMBY ? 'Emby' + : settings.currentSettings.mediaServerType === MediaServerType.PLEX + ? 'Plex' : undefined, }; @@ -73,6 +104,7 @@ const SettingsUsers = () => { { tvQuotaDays: data?.defaultQuotas.tv.quotaDays ?? 7, defaultPermissions: data?.defaultPermissions ?? 0, }} + validationSchema={schema} enableReinitialize onSubmit={async (values) => { try { @@ -90,6 +123,7 @@ const SettingsUsers = () => { }, body: JSON.stringify({ localLogin: values.localLogin, + mediaServerLogin: values.mediaServerLogin, newPlexLogin: values.newPlexLogin, defaultQuotas: { movie: { @@ -121,30 +155,61 @@ const SettingsUsers = () => { } }} > - {({ isSubmitting, values, setFieldValue }) => { + {({ isSubmitting, isValid, values, errors, setFieldValue }) => { return (
-
-
{serverType === MediaServerType.PLEX && ( <> -
+
{ setMediaServerType(MediaServerType.PLEX); setAuthToken(authToken); @@ -102,16 +100,14 @@ const SetupLogin: React.FC = ({ )} {serverType === MediaServerType.JELLYFIN && ( - )} {serverType === MediaServerType.EMBY && ( - void; + onError?: (err: string) => void; +}) { + const [loading, setLoading] = useState(false); + + const getPlexLogin = async () => { + setLoading(true); + try { + const authToken = await plexOAuth.login(); + setLoading(false); + onAuthToken(authToken); + } catch (e) { + if (onError) { + onError(e.message); + } + setLoading(false); + } + }; + + const login = () => { + plexOAuth.preparePopup(); + setTimeout(() => getPlexLogin(), 1500); + }; + + return { loading, login }; +} + +export default usePlexLogin; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index a5ce5053b..671770332 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -246,7 +246,9 @@ "components.Login.initialsigningin": "Connecting…", "components.Login.invalidurlerror": "Unable to connect to {mediaServerName} server.", "components.Login.loginerror": "Something went wrong while trying to sign in.", + "components.Login.loginwithapp": "Login with {appName}", "components.Login.noadminerror": "No admin user found on the server.", + "components.Login.orsigninwith": "Or sign in with", "components.Login.password": "Password", "components.Login.port": "Port", "components.Login.save": "Add", @@ -441,8 +443,6 @@ "components.PersonDetails.birthdate": "Born {birthdate}", "components.PersonDetails.crewmember": "Crew", "components.PersonDetails.lifespan": "{birthdate} – {deathdate}", - "components.PlexLoginButton.signingin": "Signing In…", - "components.PlexLoginButton.signinwithplex": "Sign In", "components.QuotaSelector.days": "{count, plural, one {day} other {days}}", "components.QuotaSelector.movieRequests": "{quotaLimit} {movies} per {quotaDays} {days}", "components.QuotaSelector.movies": "{count, plural, one {movie} other {movies}}", diff --git a/src/styles/globals.css b/src/styles/globals.css index 1e99d53df..287336585 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -74,15 +74,6 @@ top: env(safe-area-inset-top); } - .plex-button { - @apply flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-center text-sm font-medium text-white transition duration-150 ease-in-out disabled:opacity-50; - background-color: #cc7b19; - } - - .plex-button:hover { - background: #f19a30; - } - .server-type-button { @apply rounded-md border border-gray-500 bg-gray-700 px-4 py-2 text-white transition duration-150 ease-in-out hover:bg-gray-500; } @@ -354,9 +345,8 @@ @apply relative -ml-px inline-flex items-center border border-gray-500 bg-indigo-600 bg-opacity-80 px-3 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out last:rounded-r-md hover:bg-opacity-100 active:bg-gray-100 active:text-gray-700 sm:px-3.5; } - .button-md svg, - button.input-action svg, - .plex-button svg { + .button-md :where(svg), + button.input-action svg { @apply ml-2 mr-2 h-5 w-5 first:ml-0 last:mr-0; } From eb2bd1612e535e741fd695e01ff48ed6bfcde1ba Mon Sep 17 00:00:00 2001 From: Michael Thomas Date: Tue, 21 Jan 2025 11:25:29 -0500 Subject: [PATCH 3/3] test: update cypress login command --- cypress/support/commands.ts | 1 - src/components/Login/PlexLoginButton.tsx | 1 + src/components/Login/index.tsx | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 0eb9c869a..a23cb5e68 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -6,7 +6,6 @@ Cypress.Commands.add('login', (email, password) => { [email, password], () => { cy.visit('/login'); - cy.contains('Use your Overseerr account').click(); cy.get('[data-testid=email]').type(email); cy.get('[data-testid=password]').type(password); diff --git a/src/components/Login/PlexLoginButton.tsx b/src/components/Login/PlexLoginButton.tsx index e6c5d97fa..111b95d32 100644 --- a/src/components/Login/PlexLoginButton.tsx +++ b/src/components/Login/PlexLoginButton.tsx @@ -29,6 +29,7 @@ const PlexLoginButton = ({ className="relative flex-1 border-[#cc7b19] bg-[rgba(204,123,25,0.3)] hover:border-[#cc7b19] hover:bg-[rgba(204,123,25,0.7)] disabled:opacity-50" onClick={login} disabled={loading || isProcessing} + data-testid="plex-login-button" > {loading && (
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 7758969d2..0b51e86f1 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -137,6 +137,7 @@ const Login = () => { (mediaServerLogin ? (