From 3357343d985a4cfb4901626cf6067eb19831cb44 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Mon, 15 Feb 2021 20:29:55 -0500 Subject: [PATCH] feat(rebase): rebase --- config/db/.gitkeep | 0 overseerr-api.yml | 4 +- server/api/jellyfin.ts | 44 ++--- server/entity/Media.ts | 4 +- server/entity/User.ts | 13 +- server/job/jellyfinsync/index.ts | 15 +- server/lib/settings.ts | 8 +- server/routes/auth.ts | 100 ++++++++--- server/routes/settings/index.ts | 31 ++-- src/components/Login/AddEmailModal.tsx | 114 ++++++++++++ src/components/Login/JellyfinLogin.tsx | 240 +++++++++++++++---------- src/components/Setup/SetupLogin.tsx | 26 +-- src/components/Setup/index.tsx | 5 +- src/i18n/locale/en.json | 11 +- 14 files changed, 420 insertions(+), 195 deletions(-) create mode 100644 config/db/.gitkeep create mode 100644 src/components/Login/AddEmailModal.tsx diff --git a/config/db/.gitkeep b/config/db/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/overseerr-api.yml b/overseerr-api.yml index 8c2466fa9..8379d122d 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -2818,7 +2818,7 @@ paths: /auth/jellyfin: post: summary: Sign in using a Jellyfin username and password - description: Takes the user's username and password to log the user in. Generates a session cookie for use in further requests. If the user does not exist, and there are no other users, then a user will be created with full admin privileges. If a user logs in with access to the main Plex server, they will also have an account created, but without any permissions. + description: Takes the user's username and password to log the user in. Generates a session cookie for use in further requests. If the user does not exist, and there are no other users, then a user will be created with full admin privileges. If a user logs in with access to the Jellyfin server, they will also have an account created, but without any permissions. security: [] tags: - auth @@ -2842,6 +2842,8 @@ paths: type: string hostname: type: string + email: + type: string required: - username - password diff --git a/server/api/jellyfin.ts b/server/api/jellyfin.ts index 50dd1de67..50488cbe7 100644 --- a/server/api/jellyfin.ts +++ b/server/api/jellyfin.ts @@ -75,17 +75,16 @@ class JellyfinAPI { private jellyfinHost: string; private axios: AxiosInstance; - constructor(jellyfinHost: string, authToken?: string, userId?: string) { + constructor(jellyfinHost: string, authToken?: string, deviceId?: string) { + console.log(jellyfinHost, deviceId, authToken); this.jellyfinHost = jellyfinHost; this.authToken = authToken; - this.userId = userId; let authHeaderVal = ''; if (this.authToken) { - authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0", Token="${authToken}"`; + authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0", Token="${authToken}"`; } else { - authHeaderVal = - 'MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"'; + authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`; } this.axios = axios.create({ @@ -116,6 +115,11 @@ class JellyfinAPI { } } + public setUserId(userId: string): void { + this.userId = userId; + return; + } + public async getServerName(): Promise { try { const account = await this.axios.get( @@ -150,19 +154,21 @@ class JellyfinAPI { try { const account = await this.axios.get('/Library/MediaFolders'); - const response: JellyfinLibrary[] = []; - - account.data.Items.forEach((Item: any) => { - const library: JellyfinLibrary = { + const response: JellyfinLibrary[] = account.data.Items.filter( + (Item: any) => { + return ( + Item.Type === 'CollectionFolder' && + (Item.CollectionType === 'tvshows' || + Item.CollectionType === 'movies') + ); + } + ).map((Item: any) => { + return { key: Item.Id, title: Item.Name, - type: Item.CollectionType == 'movies' ? 'movie' : 'show', + type: Item.CollectionType === 'movies' ? 'movie' : 'show', agent: 'jellyfin', }; - - if (Item.Type == 'CollectionFolder') { - response.push(library); - } }); return response; @@ -178,9 +184,7 @@ class JellyfinAPI { public async getLibraryContents(id: string): Promise { try { const contents = await this.axios.get( - `/Users/${ - (await this.getUser()).Id - }/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}` + `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}` ); return contents.data.Items; @@ -196,9 +200,7 @@ class JellyfinAPI { public async getRecentlyAdded(id: string): Promise { try { const contents = await this.axios.get( - `/Users/${ - (await this.getUser()).Id - }/Items/Latest?Limit=50&ParentId=${id}` + `/Users/${this.userId}/Items/Latest?Limit=50&ParentId=${id}` ); return contents.data.Items; @@ -214,7 +216,7 @@ class JellyfinAPI { public async getItemData(id: string): Promise { try { const contents = await this.axios.get( - `/Users/${(await this.getUser()).Id}/Items/${id}` + `/Users/${this.userId}/Items/${id}` ); return contents.data; diff --git a/server/entity/Media.ts b/server/entity/Media.ts index af70c5165..e08a2c4ba 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -164,10 +164,10 @@ class Media { } } else { if (this.jellyfinMediaId) { - this.mediaUrl = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverID}`; + this.mediaUrl = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId}&context=home&serverId=${settings.jellyfin.serverId}`; } if (this.jellyfinMediaId4k) { - this.mediaUrl4k = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverID}`; + this.mediaUrl4k = `${settings.jellyfin.hostname}/web/#!/details?id=${this.jellyfinMediaId4k}&context=home&serverId=${settings.jellyfin.serverId}`; } } } diff --git a/server/entity/User.ts b/server/entity/User.ts index 105302bd2..aa8240bb2 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -65,16 +65,19 @@ export class User { @Column({ type: 'integer', default: UserType.PLEX }) public userType: UserType; - @Column({ nullable: true, select: false }) + @Column({ nullable: true }) public plexId?: number; - @Column({ nullable: true, select: false }) - public jellyfinId?: string; + @Column({ nullable: true }) + public jellyfinUserId?: string; - @Column({ nullable: true, select: false }) + @Column({ nullable: true }) + public jellyfinDeviceId?: string; + + @Column({ nullable: true }) public jellyfinAuthToken?: string; - @Column({ nullable: true, select: false }) + @Column({ nullable: true }) public plexToken?: string; @Column({ type: 'integer', default: 0 }) diff --git a/server/job/jellyfinsync/index.ts b/server/job/jellyfinsync/index.ts index b4c7db0c0..fb4d393d2 100644 --- a/server/job/jellyfinsync/index.ts +++ b/server/job/jellyfinsync/index.ts @@ -264,7 +264,7 @@ class JobJellyfinSync { ExtendedEpisodeData.MediaSources?.some((MediaSource) => { return MediaSource.MediaStreams.some((MediaStream) => { - if (MediaStream.Type == 'Video') { + if (MediaStream.Type === 'Video') { if (MediaStream.Width ?? 0 < 2000) { totalStandard++; } @@ -552,7 +552,12 @@ class JobJellyfinSync { this.running = true; const userRepository = getRepository(User); const admin = await userRepository.findOne({ - select: ['id', 'jellyfinAuthToken', 'jellyfinId'], + select: [ + 'id', + 'jellyfinAuthToken', + 'jellyfinUserId', + 'jellyfinDeviceId', + ], order: { id: 'ASC' }, }); @@ -562,10 +567,12 @@ class JobJellyfinSync { this.jfClient = new JellyfinAPI( settings.jellyfin.hostname ?? '', - admin.jellyfinAuthToken ?? '', - admin.jellyfinId ?? '' + admin.jellyfinAuthToken, + admin.jellyfinDeviceId ); + this.jfClient.setUserId(admin.jellyfinUserId ?? ''); + this.libraries = settings.jellyfin.libraries.filter( (library) => library.enabled ); diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 0656f9e84..a988bfd6a 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -35,9 +35,7 @@ export interface JellyfinSettings { name: string; hostname?: string; libraries: Library[]; - adminUser: string; - adminPass: string; - serverID: string; + serverId: string; } interface DVRSettings { @@ -223,9 +221,7 @@ class Settings { name: '', hostname: '', libraries: [], - adminUser: '', - adminPass: '', - serverID: '', + serverId: '', }, radarr: [], sonarr: [], diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 71218c9ef..068433b8e 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -43,7 +43,7 @@ authRoutes.post('/plex', async (req, res, next) => { settings.main.mediaServerType != MediaServerType.PLEX && settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED ) { - return res.status(500).json({ error: 'Plex login disabled' }); + return res.status(500).json({ error: 'Plex login is disabled' }); } try { // First we need to use this auth token to get the users email from plex.tv @@ -154,40 +154,52 @@ authRoutes.post('/jellyfin', async (req, res, next) => { username?: string; password?: string; hostname?: string; + email?: string; }; //Make sure jellyfin login is enabled, but only if jellyfin is not already configured if ( - settings.main.mediaServerType != MediaServerType.JELLYFIN && - settings.jellyfin.hostname != '' + settings.main.mediaServerType !== MediaServerType.JELLYFIN && + settings.jellyfin.hostname !== '' ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); } else if (!body.username || !body.password) { return res .status(500) .json({ error: 'You must provide an username and a password' }); - } else if (settings.jellyfin.hostname != '' && body.hostname) { + } else if (settings.jellyfin.hostname !== '' && body.hostname) { return res .status(500) .json({ error: 'Jellyfin hostname already configured' }); - } else if (settings.jellyfin.hostname == '' && !body.hostname) { + } else if (settings.jellyfin.hostname === '' && !body.hostname) { return res.status(500).json({ error: 'No hostname provided.' }); } try { const hostname = - settings.jellyfin.hostname != '' + settings.jellyfin.hostname !== '' ? settings.jellyfin.hostname : body.hostname; + // Try to find deviceId that corresponds to jellyfin user, else generate a new one + let user = await userRepository.findOne({ + where: { jellyfinUsername: body.username }, + }); + + let deviceId = ''; + if (user) { + deviceId = user.jellyfinDeviceId ?? ''; + } else { + deviceId = Buffer.from( + `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0|${Date.now()}` + ).toString('base64'); + } // First we need to attempt to log the user in to jellyfin - const jellyfinserver = new JellyfinAPI(hostname ?? ''); - settings.jellyfin.name = await jellyfinserver.getServerName(); + const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); const account = await jellyfinserver.login(body.username, body.password); - // Next let's see if the user already exists - let user = await userRepository.findOne({ - where: { jellyfinId: account.User.Id }, + user = await userRepository.findOne({ + where: { jellyfinUserId: account.User.Id }, }); if (user) { @@ -200,9 +212,9 @@ authRoutes.post('/jellyfin', async (req, res, next) => { if (typeof account.User.PrimaryImageTag !== undefined) { user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; } else { - user.avatar = '/images/os_logo_square.png'; + user.avatar = '/os_logo_square.png'; } - user.email = account.User.Name; + user.jellyfinUsername = account.User.Name; if (user.username === account.User.Name) { @@ -213,30 +225,52 @@ authRoutes.post('/jellyfin', async (req, res, next) => { // Here we check if it's the first user. If it is, we create the user with no check // and give them admin permissions const totalUsers = await userRepository.count(); - if (totalUsers === 0) { user = new User({ - email: account.User.Name, + email: body.email, jellyfinUsername: account.User.Name, - jellyfinId: account.User.Id, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, jellyfinAuthToken: account.AccessToken, permissions: Permission.ADMIN, avatar: typeof account.User.PrimaryImageTag !== undefined ? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : '/images/os_logo_square.png', + : '/os_logo_square.png', userType: UserType.JELLYFIN, }); await userRepository.save(user); //Update hostname in settings if it doesn't exist (initial configuration) //Also set mediaservertype to JELLYFIN - if (settings.jellyfin.hostname == '') { + if (settings.jellyfin.hostname === '') { settings.main.mediaServerType = MediaServerType.JELLYFIN; settings.jellyfin.hostname = body.hostname ?? ''; + settings.jellyfin.serverId = account.User.ServerId; settings.save(); } } + + if (!user) { + if (!body.email) { + throw new Error('add_email'); + } + + user = new User({ + email: body.email, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: settings.main.defaultPermissions, + avatar: + typeof account.User.PrimaryImageTag !== undefined + ? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : '/os_logo_square.png', + userType: UserType.JELLYFIN, + }); + await userRepository.save(user); + } } // Set logged in session @@ -246,16 +280,32 @@ authRoutes.post('/jellyfin', async (req, res, next) => { return res.status(200).json(user?.filter() ?? {}); } catch (e) { - if (e.message != 'Unauthorized') { + if (e.message === 'Unauthorized') { + logger.info( + 'Failed login attempt from user with incorrect Jellyfin credentials', + { + label: 'Auth', + account: { + ip: req.ip, + email: body.username, + password: '__REDACTED__', + }, + } + ); + return next({ + status: 401, + message: 'Unauthorized', + }); + } else if (e.message === 'add_email') { + return next({ + status: 406, + message: 'CREDENTIAL_ERROR_ADD_EMAIL', + }); + } else { logger.error(e.message, { label: 'Auth' }); return next({ status: 500, - message: 'Something went wrong. Is your auth token valid?', - }); - } else { - return next({ - status: 401, - message: 'CREDENTIAL_ERROR', + message: 'Something went wrong.', }); } } diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts index e50d38c90..e893ff2a7 100644 --- a/server/routes/settings/index.ts +++ b/server/routes/settings/index.ts @@ -228,8 +228,7 @@ settingsRoutes.post('/plex/sync', (req, res) => { settingsRoutes.get('/jellyfin', (_req, res) => { const settings = getSettings(); - //DO NOT RETURN ADMIN USER CREDENTIALS!! - res.status(200).json(omit(settings.jellyfin, ['adminUser', 'adminPass'])); + res.status(200).json(settings.jellyfin); }); settingsRoutes.post('/jellyfin', (req, res) => { @@ -247,30 +246,28 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => { if (req.query.sync) { const userRepository = getRepository(User); const admin = await userRepository.findOneOrFail({ - select: ['id', 'jellyfinAuthToken'], + select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId'], order: { id: 'ASC' }, }); const jellyfinClient = new JellyfinAPI( settings.jellyfin.hostname ?? '', - admin.jellyfinAuthToken ?? '' + admin.jellyfinAuthToken ?? '', + admin.jellyfinDeviceId ?? '' ); const libraries = await jellyfinClient.getLibraries(); - const newLibraries: Library[] = libraries - // Remove libraries that are not movie or show - .filter((library) => library.type === 'movie' || library.type === 'show') - .map((library) => { - const existing = settings.plex.libraries.find( - (l) => l.id === library.key && l.name === library.title - ); + const newLibraries: Library[] = libraries.map((library) => { + const existing = settings.jellyfin.libraries.find( + (l) => l.id === library.key && l.name === library.title + ); - return { - id: library.key, - name: library.title, - enabled: existing?.enabled ?? false, - }; - }); + return { + id: library.key, + name: library.title, + enabled: existing?.enabled ?? false, + }; + }); settings.jellyfin.libraries = newLibraries; } diff --git a/src/components/Login/AddEmailModal.tsx b/src/components/Login/AddEmailModal.tsx new file mode 100644 index 000000000..f486d9103 --- /dev/null +++ b/src/components/Login/AddEmailModal.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import Transition from '../Transition'; +import Modal from '../Common/Modal'; +import { Formik, Field } from 'formik'; +import * as Yup from 'yup'; +import axios from 'axios'; +import { defineMessages, useIntl } from 'react-intl'; +import useSettings from '../../hooks/useSettings'; + +const messages = defineMessages({ + title: 'Add Email', + description: + 'Since this is your first time logging into {applicationName}, you are required to add a valid email address.', + email: 'Email address', + validationEmailRequired: 'You must provide an email', + validationEmailFormat: 'Invalid email', + saving: 'Adding…', + save: 'Add', +}); + +interface AddEmailModalProps { + username: string; + password: string; + onClose: () => void; + onSave: () => void; +} + +const AddEmailModal: React.FC = ({ + onClose, + username, + password, + onSave, +}) => { + const intl = useIntl(); + const settings = useSettings(); + + const EmailSettingsSchema = Yup.object().shape({ + email: Yup.string() + .email(intl.formatMessage(messages.validationEmailFormat)) + .required(intl.formatMessage(messages.validationEmailRequired)), + }); + + return ( + + { + try { + await axios.post('/api/v1/auth/jellyfin', { + username: username, + password: password, + email: values.email, + }); + + onSave(); + } catch (e) { + // set error here + } + }} + > + {({ errors, touched, handleSubmit, isSubmitting, isValid }) => { + return ( + handleSubmit()} + title={intl.formatMessage(messages.title)} + > + {intl.formatMessage(messages.description, { + applicationName: settings.currentSettings.applicationTitle, + })} + +
+
+ +
+ {errors.email && touched.email && ( +
{errors.email}
+ )} +
+
+ ); + }} +
+
+ ); +}; + +export default AddEmailModal; diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index 1fc5d88c2..6c943223f 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import Button from '../Common/Button'; @@ -7,13 +7,17 @@ import * as Yup from 'yup'; import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; import useSettings from '../../hooks/useSettings'; +import AddEmailModal from './AddEmailModal'; const messages = defineMessages({ username: 'Username', password: 'Password', host: 'Jellyfin URL', + email: 'Email', validationhostrequired: 'Jellyfin URL required', validationhostformat: 'Valid URL required', + validationemailrequired: 'Email required', + validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', loginerror: 'Something went wrong while trying to sign in.', @@ -34,6 +38,9 @@ const JellyfinLogin: React.FC = ({ revalidate, initial, }) => { + const [requiresEmail, setRequiresEmail] = useState(0); + const [username, setUsername] = useState(); + const [password, setPassword] = useState(); const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); @@ -43,6 +50,9 @@ const JellyfinLogin: React.FC = ({ host: Yup.string() .url(intl.formatMessage(messages.validationhostformat)) .required(intl.formatMessage(messages.validationhostrequired)), + email: Yup.string() + .email(intl.formatMessage(messages.validationemailformat)) + .required(intl.formatMessage(messages.validationemailrequired)), username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), @@ -56,6 +66,7 @@ const JellyfinLogin: React.FC = ({ username: '', password: '', host: '', + email: '', }} validationSchema={LoginSchema} onSubmit={async (values) => { @@ -64,6 +75,7 @@ const JellyfinLogin: React.FC = ({ username: values.username, password: values.password, hostname: values.host, + email: values.email, }); } catch (e) { toasts.addToast( @@ -101,6 +113,22 @@ const JellyfinLogin: React.FC = ({
{errors.host}
)} + +
+
+ +
+ {errors.email && touched.email && ( +
{errors.email}
+ )} +
@@ -163,105 +191,121 @@ const JellyfinLogin: React.FC = ({ ), }); return ( - { - try { - await axios.post('/api/v1/auth/jellyfin', { - username: values.username, - password: values.password, - }); - } catch (e) { - toasts.addToast( - intl.formatMessage( - e.message == 'Request failed with status code 401' - ? messages.credentialerror - : messages.loginerror - ), - { - autoDismiss: true, - appearance: 'error', +
+ {requiresEmail == 1 && ( + setRequiresEmail(0)} + > + )} + { + try { + await axios.post('/api/v1/auth/jellyfin', { + username: values.username, + password: values.password, + }); + } catch (e) { + if (e.message === 'Request failed with status code 406') { + setUsername(values.username); + setPassword(values.password); + setRequiresEmail(1); + } else { + 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}
+ )} +
+
+
+
+ + + + + + +
+
+
+ ); - } finally { - revalidate(); - } - }} - > - {({ errors, touched, isSubmitting, isValid }) => { - return ( - <> -
-
- -
-
- -
- {errors.username && touched.username && ( -
{errors.username}
- )} -
- -
-
- -
- {errors.password && touched.password && ( -
{errors.password}
- )} -
-
-
-
- - - - - - -
-
-
- - ); - }} -
+ }} + +
); } }; diff --git a/src/components/Setup/SetupLogin.tsx b/src/components/Setup/SetupLogin.tsx index 5566a6eec..5d5854d6e 100644 --- a/src/components/Setup/SetupLogin.tsx +++ b/src/components/Setup/SetupLogin.tsx @@ -62,18 +62,18 @@ const SetupLogin: React.FC = ({ onComplete }) => { {({ openIndexes, handleClick, AccordionContent }) => ( <> - -
+ +
{ setMediaServerType(MediaServerType.PLEX); @@ -84,7 +84,7 @@ const SetupLogin: React.FC = ({ onComplete }) => {
- -
+ +
diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index 2219964ef..b084cc275 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -104,7 +104,10 @@ const Setup: React.FC = () => { /> -
+
{currentStep === 1 && ( { diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index db5a1839a..2ea973d0b 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -37,6 +37,7 @@ "components.Layout.UserDropdown.signout": "Sign Out", "components.Layout.alphawarning": "This is ALPHA software. Features may be broken and/or unstable. Please report issues on GitHub!", "components.Login.credentialerror": "The username or password is incorrect.", + "components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.", "components.Login.email": "Email Address", "components.Login.forgotpassword": "Forgot Password?", "components.Login.host": "Jellyfin URL", @@ -44,13 +45,19 @@ "components.Login.initialsigningin": "Connecting…", "components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.password": "Password", + "components.Login.save": "Add", + "components.Login.saving": "Adding…", "components.Login.signin": "Sign In", "components.Login.signingin": "Signing in…", "components.Login.signinheader": "Sign in to continue", "components.Login.signinwithjellyfin": "Use your Jellyfin account", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithplex": "Use your Plex account", + "components.Login.title": "Add Email", "components.Login.username": "Username", + "components.Login.validationEmailFormat": "Invalid email", + "components.Login.validationEmailRequired": "You must provide an email", + "components.Login.validationemailformat": "Valid email required", "components.Login.validationemailrequired": "You must provide a valid email address", "components.Login.validationhostformat": "Valid URL required", "components.Login.validationhostrequired": "Jellyfin URL required", @@ -598,8 +605,8 @@ "components.Setup.setup": "Setup", "components.Setup.signin": "Sign In", "components.Setup.signinMessage": "Get started by signing in", - "components.Setup.signinWithJellyfin": "Use Jellyfin", - "components.Setup.signinWithPlex": "Sign in with Plex", + "components.Setup.signinWithJellyfin": "Use your Jellyfin account", + "components.Setup.signinWithPlex": "Use your Plex account", "components.Setup.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.", "components.Setup.tip": "Tip", "components.Setup.welcome": "Welcome to Overseerr",