Adding Jellyfin Setting for Custom "Forgot Password" URL

Adding Jellyfin Setting for Custom "Forgot Password" URL.  Useful in cases where you are using a custom authentication provider such as the LDAP plugin, Authelia, lldap, or any other external auth scheme with its own password reset page.
This commit is contained in:
Derek Paschal
2023-11-14 08:20:28 -06:00
parent 4005397f3d
commit ce9802d5d4
6 changed files with 68 additions and 31 deletions

View File

@@ -368,6 +368,9 @@ components:
externalHostname: externalHostname:
type: string type: string
example: 'http://my.jellyfin.host' example: 'http://my.jellyfin.host'
jellyfinForgotPasswordUrl:
type: string
example: 'http://my.jellyfin.host/web/index.html#!/forgotpassword.html'
adminUser: adminUser:
type: string type: string
example: 'admin' example: 'admin'

View File

@@ -41,6 +41,7 @@ export interface PublicSettingsResponse {
locale: string; locale: string;
emailEnabled: boolean; emailEnabled: boolean;
newPlexLogin: boolean; newPlexLogin: boolean;
jellyfinForgotPasswordUrl: string;
} }
export interface CacheItem { export interface CacheItem {

View File

@@ -42,6 +42,7 @@ export interface JellyfinSettings {
externalHostname?: string; externalHostname?: string;
libraries: Library[]; libraries: Library[];
serverId: string; serverId: string;
jellyfinForgotPasswordUrl: string;
} }
export interface TautulliSettings { export interface TautulliSettings {
hostname?: string; hostname?: string;
@@ -124,6 +125,7 @@ interface FullPublicSettings extends PublicSettings {
applicationUrl: string; applicationUrl: string;
hideAvailable: boolean; hideAvailable: boolean;
localLogin: boolean; localLogin: boolean;
jellyfinForgotPasswordUrl: string;
movie4kEnabled: boolean; movie4kEnabled: boolean;
series4kEnabled: boolean; series4kEnabled: boolean;
region: string; region: string;
@@ -331,6 +333,7 @@ class Settings {
name: '', name: '',
hostname: '', hostname: '',
externalHostname: '', externalHostname: '',
jellyfinForgotPasswordUrl: '',
libraries: [], libraries: [],
serverId: '', serverId: '',
}, },
@@ -534,6 +537,7 @@ class Settings {
applicationUrl: this.data.main.applicationUrl, applicationUrl: this.data.main.applicationUrl,
hideAvailable: this.data.main.hideAvailable, hideAvailable: this.data.main.hideAvailable,
localLogin: this.data.main.localLogin, localLogin: this.data.main.localLogin,
jellyfinForgotPasswordUrl: this.data.jellyfin.jellyfinForgotPasswordUrl,
movie4kEnabled: this.data.radarr.some( movie4kEnabled: this.data.radarr.some(
(radarr) => radarr.is4k && radarr.isDefault (radarr) => radarr.is4k && radarr.isDefault
), ),

View File

@@ -222,6 +222,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
const baseUrl = settings.currentSettings.jellyfinExternalHost const baseUrl = settings.currentSettings.jellyfinExternalHost
? settings.currentSettings.jellyfinExternalHost ? settings.currentSettings.jellyfinExternalHost
: settings.currentSettings.jellyfinHost; : settings.currentSettings.jellyfinHost;
const jellyfinForgotPasswordUrl = settings.currentSettings.jellyfinForgotPasswordUrl;
return ( return (
<div> <div>
<Formik <Formik
@@ -298,11 +299,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
<Button <Button
as="a" as="a"
buttonType="ghost" buttonType="ghost"
href={`${baseUrl}/web/index.html#!/${ href={jellyfinForgotPasswordUrl ? `${jellyfinForgotPasswordUrl}` :
process.env.JELLYFIN_TYPE === 'emby' `${baseUrl}/web/index.html#!/${process.env.JELLYFIN_TYPE === 'emby'
? 'startup/' ? 'startup/'
: '' : ''
}forgotpassword.html`} }forgotpassword.html`}
> >
{intl.formatMessage(messages.forgotpassword)} {intl.formatMessage(messages.forgotpassword)}
</Button> </Button>

View File

@@ -30,9 +30,10 @@ const messages = defineMessages({
jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!', jellyfinSettingsSuccess: '{mediaServerName} settings saved successfully!',
jellyfinSettings: '{mediaServerName} Settings', jellyfinSettings: '{mediaServerName} Settings',
jellyfinSettingsDescription: jellyfinSettingsDescription:
'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL.', 'Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.',
externalUrl: 'External URL', externalUrl: 'External URL',
internalUrl: 'Internal URL', internalUrl: 'Internal URL',
jellyfinForgotPasswordUrl: 'Forgot Password URL',
validationUrl: 'You must provide a valid URL', validationUrl: 'You must provide a valid URL',
syncing: 'Syncing', syncing: 'Syncing',
syncJellyfin: 'Sync Libraries', syncJellyfin: 'Sync Libraries',
@@ -94,6 +95,10 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm, /^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
intl.formatMessage(messages.validationUrl) intl.formatMessage(messages.validationUrl)
), ),
jellyfinForgotPasswordUrl: Yup.string().matches(
/^(https?:\/\/)?(?:[\w-]+\.)*[\w-]+(?::\d{2,5})?(?:\/[\w-]+)*(?:\/)?$/gm,
intl.formatMessage(messages.validationUrl)
),
}); });
const activeLibraries = const activeLibraries =
@@ -171,20 +176,20 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<h3 className="heading"> <h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibraries, { ? intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
: intl.formatMessage(messages.jellyfinlibraries, { : intl.formatMessage(messages.jellyfinlibraries, {
mediaServerName: 'Jellyfin', mediaServerName: 'Jellyfin',
})} })}
</h3> </h3>
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinlibrariesDescription, { ? intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
: intl.formatMessage(messages.jellyfinlibrariesDescription, { : intl.formatMessage(messages.jellyfinlibrariesDescription, {
mediaServerName: 'Jellyfin', mediaServerName: 'Jellyfin',
})} })}
</p> </p>
</div> </div>
<div className="section"> <div className="section">
@@ -223,11 +228,11 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.manualscanDescriptionJellyfin, { ? intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
: intl.formatMessage(messages.manualscanDescriptionJellyfin, { : intl.formatMessage(messages.manualscanDescriptionJellyfin, {
mediaServerName: 'Jellyfin', mediaServerName: 'Jellyfin',
})} })}
</p> </p>
</div> </div>
<div className="section"> <div className="section">
@@ -271,11 +276,11 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
values={{ values={{
count: dataSync.currentLibrary count: dataSync.currentLibrary
? dataSync.libraries.slice( ? dataSync.libraries.slice(
dataSync.libraries.findIndex( dataSync.libraries.findIndex(
(library) => (library) =>
library.id === dataSync.currentLibrary?.id library.id === dataSync.currentLibrary?.id
) + 1 ) + 1
).length ).length
: 0, : 0,
}} }}
/> />
@@ -333,26 +338,27 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
<h3 className="heading"> <h3 className="heading">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettings, { ? intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
: intl.formatMessage(messages.jellyfinSettings, { : intl.formatMessage(messages.jellyfinSettings, {
mediaServerName: 'Jellyfin', mediaServerName: 'Jellyfin',
})} })}
</h3> </h3>
<p className="description"> <p className="description">
{publicRuntimeConfig.JELLYFIN_TYPE == 'emby' {publicRuntimeConfig.JELLYFIN_TYPE == 'emby'
? intl.formatMessage(messages.jellyfinSettingsDescription, { ? intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Emby', mediaServerName: 'Emby',
}) })
: intl.formatMessage(messages.jellyfinSettingsDescription, { : intl.formatMessage(messages.jellyfinSettingsDescription, {
mediaServerName: 'Jellyfin', mediaServerName: 'Jellyfin',
})} })}
</p> </p>
</div> </div>
<Formik <Formik
initialValues={{ initialValues={{
jellyfinInternalUrl: data?.hostname || '', jellyfinInternalUrl: data?.hostname || '',
jellyfinExternalUrl: data?.externalHostname || '', jellyfinExternalUrl: data?.externalHostname || '',
jellyfinForgotPasswordUrl: data?.jellyfinForgotPasswordUrl || '',
}} }}
validationSchema={JellyfinSettingsSchema} validationSchema={JellyfinSettingsSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
@@ -360,6 +366,7 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
await axios.post('/api/v1/settings/jellyfin', { await axios.post('/api/v1/settings/jellyfin', {
hostname: values.jellyfinInternalUrl, hostname: values.jellyfinInternalUrl,
externalHostname: values.jellyfinExternalUrl, externalHostname: values.jellyfinExternalUrl,
jellyfinForgotPasswordUrl: values.jellyfinForgotPasswordUrl,
} as JellyfinSettings); } as JellyfinSettings);
addToast( addToast(
@@ -437,6 +444,27 @@ const SettingsJellyfin: React.FC<SettingsJellyfinProps> = ({
)} )}
</div> </div>
</div> </div>
<div className="form-row">
<label htmlFor="jellyfinForgotPasswordUrl" className="text-label">
{intl.formatMessage(messages.jellyfinForgotPasswordUrl)}
</label>
<div className="form-input-area">
<div className="form-input-field">
<Field
type="text"
inputMode="url"
id="jellyfinForgotPasswordUrl"
name="jellyfinForgotPasswordUrl"
/>
</div>
{errors.jellyfinForgotPasswordUrl &&
touched.jellyfinForgotPasswordUrl && (
<div className="error">
{errors.jellyfinForgotPasswordUrl}
</div>
)}
</div>
</div>
<div className="actions"> <div className="actions">
<div className="flex justify-end"> <div className="flex justify-end">
<span className="ml-3 inline-flex rounded-md shadow-sm"> <span className="ml-3 inline-flex rounded-md shadow-sm">

View File

@@ -938,7 +938,7 @@
"components.Settings.internalUrl": "Internal URL", "components.Settings.internalUrl": "Internal URL",
"components.Settings.is4k": "4K", "components.Settings.is4k": "4K",
"components.Settings.jellyfinSettings": "{mediaServerName} Settings", "components.Settings.jellyfinSettings": "{mediaServerName} Settings",
"components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL.", "components.Settings.jellyfinSettingsDescription": "Optionally configure the internal and external endpoints for your {mediaServerName} server. In most cases, the external URL is different to the internal URL. A custom password reset URL can also be set for {mediaServerName} login, in case you would like to redirect to a different password reset page.",
"components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.", "components.Settings.jellyfinSettingsFailure": "Something went wrong while saving {mediaServerName} settings.",
"components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!", "components.Settings.jellyfinSettingsSuccess": "{mediaServerName} settings saved successfully!",
"components.Settings.jellyfinlibraries": "{mediaServerName} Libraries", "components.Settings.jellyfinlibraries": "{mediaServerName} Libraries",