import Button from '@app/components/Common/Button'; import Tooltip from '@app/components/Common/Tooltip'; import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { ApiErrorCode } from '@server/constants/error'; import { Field, Form, Formik } from 'formik'; import getConfig from 'next/config'; import { useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import * as Yup from 'yup'; const messages = defineMessages('components.Login', { username: 'Username', password: 'Password', hostname: '{mediaServerName} URL', port: 'Port', enablessl: 'Use SSL', urlBase: 'URL Base', email: 'Email', emailtooltip: 'Address does not need to be associated with your {mediaServerName} instance.', validationhostrequired: '{mediaServerName} URL required', validationhostformat: 'Valid URL required', validationemailrequired: 'Email required', validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', validationHostnameRequired: 'You must provide a valid hostname or IP address', validationPortRequired: 'You must provide a valid port number', validationUrlTrailingSlash: 'URL must not end in a trailing slash', validationUrlBaseLeadingSlash: 'URL base must have a leading slash', validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash', loginerror: 'Something went wrong while trying to sign in.', adminerror: 'You must use an admin account to sign in.', credentialerror: 'The username or password is incorrect.', invalidurlerror: 'Unable to connect to {mediaServerName} server.', signingin: 'Signing in…', signin: 'Sign In', initialsigningin: 'Connecting…', initialsignin: 'Connect', forgotpassword: 'Forgot Password?', }); interface JellyfinLoginProps { revalidate: () => void; initial?: boolean; } const JellyfinLogin: React.FC = ({ revalidate, initial, }) => { const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); const { publicRuntimeConfig } = getConfig(); if (initial) { const LoginSchema = Yup.object().shape({ hostname: Yup.string().required( intl.formatMessage(messages.validationhostrequired, { mediaServerName: publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', }) ), port: Yup.number().required( intl.formatMessage(messages.validationPortRequired) ), urlBase: Yup.string() .matches( /^(\/[^/].*[^/]$)/, intl.formatMessage(messages.validationUrlBaseLeadingSlash) ) .matches( /^(.*[^/])$/, intl.formatMessage(messages.validationUrlBaseTrailingSlash) ), email: Yup.string() .email(intl.formatMessage(messages.validationemailformat)) .required(intl.formatMessage(messages.validationemailrequired)), username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), password: Yup.string(), }); const mediaServerFormatValues = { mediaServerName: publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', }; return ( { try { const res = await fetch('/api/v1/auth/jellyfin', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: values.username, password: values.password, hostname: values.hostname, port: values.port, useSsl: values.useSsl, urlBase: values.urlBase, email: values.email, }), }); if (!res.ok) throw new Error(res.statusText, { cause: res }); } catch (e) { let errorData; try { errorData = await e.cause?.text(); errorData = JSON.parse(errorData); } catch { /* empty */ } let errorMessage = null; switch (errorData?.message) { case ApiErrorCode.InvalidUrl: errorMessage = messages.invalidurlerror; break; case ApiErrorCode.InvalidCredentials: errorMessage = messages.credentialerror; break; case ApiErrorCode.NotAdmin: errorMessage = messages.adminerror; break; default: errorMessage = messages.loginerror; break; } toasts.addToast( intl.formatMessage(errorMessage, mediaServerFormatValues), { autoDismiss: true, appearance: 'error', } ); } finally { revalidate(); } }} > {({ errors, touched, values, setFieldValue, isSubmitting, isValid, }) => (
{values.useSsl ? 'https://' : 'http://'}
{errors.hostname && touched.hostname && (
{errors.hostname}
)}
{errors.port && touched.port && (
{errors.port}
)}
{ setFieldValue('useSsl', !values.useSsl); setFieldValue('port', values.useSsl ? 8096 : 443); }} />
{errors.urlBase && touched.urlBase && (
{errors.urlBase}
)}
{errors.email && touched.email && (
{errors.email}
)}
{errors.username && touched.username && (
{errors.username}
)}
{errors.password && touched.password && (
{errors.password}
)}
)}
); } else { const LoginSchema = Yup.object().shape({ username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), password: Yup.string(), }); const baseUrl = settings.currentSettings.jellyfinExternalHost ? settings.currentSettings.jellyfinExternalHost : settings.currentSettings.jellyfinHost; const jellyfinForgotPasswordUrl = settings.currentSettings.jellyfinForgotPasswordUrl; return (
{ try { const res = await fetch('/api/v1/auth/jellyfin', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ username: values.username, password: values.password, email: values.username, }), }); if (!res.ok) throw new Error(); } catch (e) { toasts.addToast( intl.formatMessage( e.message == 'Request failed with status code 401' ? messages.credentialerror : messages.loginerror ), { autoDismiss: true, appearance: 'error', } ); } finally { revalidate(); } }} > {({ errors, touched, isSubmitting, isValid }) => { return ( <>
{errors.username && touched.username && (
{errors.username}
)}
{errors.password && touched.password && (
{errors.password}
)}
); }}
); } }; export default JellyfinLogin;