feat(rebase): rebase

This commit is contained in:
Aiden Vigue
2021-02-15 20:29:55 -05:00
parent 9d61092f37
commit 3357343d98
14 changed files with 420 additions and 195 deletions

0
config/db/.gitkeep Normal file
View File

View File

@@ -2818,7 +2818,7 @@ paths:
/auth/jellyfin: /auth/jellyfin:
post: post:
summary: Sign in using a Jellyfin username and password 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: [] security: []
tags: tags:
- auth - auth
@@ -2842,6 +2842,8 @@ paths:
type: string type: string
hostname: hostname:
type: string type: string
email:
type: string
required: required:
- username - username
- password - password

View File

@@ -75,17 +75,16 @@ class JellyfinAPI {
private jellyfinHost: string; private jellyfinHost: string;
private axios: AxiosInstance; 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.jellyfinHost = jellyfinHost;
this.authToken = authToken; this.authToken = authToken;
this.userId = userId;
let authHeaderVal = ''; let authHeaderVal = '';
if (this.authToken) { 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 { } else {
authHeaderVal = authHeaderVal = `MediaBrowser Client="Overseerr", Device="Axios", DeviceId="${deviceId}", Version="10.8.0"`;
'MediaBrowser Client="Overseerr", Device="Axios", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NDsgcnY6ODUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC84NS4wfDE2MTI5MjcyMDM5NzM1", Version="10.8.0"';
} }
this.axios = axios.create({ this.axios = axios.create({
@@ -116,6 +115,11 @@ class JellyfinAPI {
} }
} }
public setUserId(userId: string): void {
this.userId = userId;
return;
}
public async getServerName(): Promise<string> { public async getServerName(): Promise<string> {
try { try {
const account = await this.axios.get<JellyfinUserResponse>( const account = await this.axios.get<JellyfinUserResponse>(
@@ -150,19 +154,21 @@ class JellyfinAPI {
try { try {
const account = await this.axios.get<any>('/Library/MediaFolders'); const account = await this.axios.get<any>('/Library/MediaFolders');
const response: JellyfinLibrary[] = []; const response: JellyfinLibrary[] = account.data.Items.filter(
(Item: any) => {
account.data.Items.forEach((Item: any) => { return (
const library: JellyfinLibrary = { Item.Type === 'CollectionFolder' &&
(Item.CollectionType === 'tvshows' ||
Item.CollectionType === 'movies')
);
}
).map((Item: any) => {
return <JellyfinLibrary>{
key: Item.Id, key: Item.Id,
title: Item.Name, title: Item.Name,
type: Item.CollectionType == 'movies' ? 'movie' : 'show', type: Item.CollectionType === 'movies' ? 'movie' : 'show',
agent: 'jellyfin', agent: 'jellyfin',
}; };
if (Item.Type == 'CollectionFolder') {
response.push(library);
}
}); });
return response; return response;
@@ -178,9 +184,7 @@ class JellyfinAPI {
public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> { public async getLibraryContents(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/Users/${ `/Users/${this.userId}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}`
(await this.getUser()).Id
}/Items?SortBy=SortName&SortOrder=Ascending&Recursive=true&StartIndex=0&ParentId=${id}`
); );
return contents.data.Items; return contents.data.Items;
@@ -196,9 +200,7 @@ class JellyfinAPI {
public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> { public async getRecentlyAdded(id: string): Promise<JellyfinLibraryItem[]> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/Users/${ `/Users/${this.userId}/Items/Latest?Limit=50&ParentId=${id}`
(await this.getUser()).Id
}/Items/Latest?Limit=50&ParentId=${id}`
); );
return contents.data.Items; return contents.data.Items;
@@ -214,7 +216,7 @@ class JellyfinAPI {
public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> { public async getItemData(id: string): Promise<JellyfinLibraryItemExtended> {
try { try {
const contents = await this.axios.get<any>( const contents = await this.axios.get<any>(
`/Users/${(await this.getUser()).Id}/Items/${id}` `/Users/${this.userId}/Items/${id}`
); );
return contents.data; return contents.data;

View File

@@ -164,10 +164,10 @@ class Media {
} }
} else { } else {
if (this.jellyfinMediaId) { 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) { 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}`;
} }
} }
} }

View File

@@ -65,16 +65,19 @@ export class User {
@Column({ type: 'integer', default: UserType.PLEX }) @Column({ type: 'integer', default: UserType.PLEX })
public userType: UserType; public userType: UserType;
@Column({ nullable: true, select: false }) @Column({ nullable: true })
public plexId?: number; public plexId?: number;
@Column({ nullable: true, select: false }) @Column({ nullable: true })
public jellyfinId?: string; public jellyfinUserId?: string;
@Column({ nullable: true, select: false }) @Column({ nullable: true })
public jellyfinDeviceId?: string;
@Column({ nullable: true })
public jellyfinAuthToken?: string; public jellyfinAuthToken?: string;
@Column({ nullable: true, select: false }) @Column({ nullable: true })
public plexToken?: string; public plexToken?: string;
@Column({ type: 'integer', default: 0 }) @Column({ type: 'integer', default: 0 })

View File

@@ -264,7 +264,7 @@ class JobJellyfinSync {
ExtendedEpisodeData.MediaSources?.some((MediaSource) => { ExtendedEpisodeData.MediaSources?.some((MediaSource) => {
return MediaSource.MediaStreams.some((MediaStream) => { return MediaSource.MediaStreams.some((MediaStream) => {
if (MediaStream.Type == 'Video') { if (MediaStream.Type === 'Video') {
if (MediaStream.Width ?? 0 < 2000) { if (MediaStream.Width ?? 0 < 2000) {
totalStandard++; totalStandard++;
} }
@@ -552,7 +552,12 @@ class JobJellyfinSync {
this.running = true; this.running = true;
const userRepository = getRepository(User); const userRepository = getRepository(User);
const admin = await userRepository.findOne({ const admin = await userRepository.findOne({
select: ['id', 'jellyfinAuthToken', 'jellyfinId'], select: [
'id',
'jellyfinAuthToken',
'jellyfinUserId',
'jellyfinDeviceId',
],
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
@@ -562,10 +567,12 @@ class JobJellyfinSync {
this.jfClient = new JellyfinAPI( this.jfClient = new JellyfinAPI(
settings.jellyfin.hostname ?? '', settings.jellyfin.hostname ?? '',
admin.jellyfinAuthToken ?? '', admin.jellyfinAuthToken,
admin.jellyfinId ?? '' admin.jellyfinDeviceId
); );
this.jfClient.setUserId(admin.jellyfinUserId ?? '');
this.libraries = settings.jellyfin.libraries.filter( this.libraries = settings.jellyfin.libraries.filter(
(library) => library.enabled (library) => library.enabled
); );

View File

@@ -35,9 +35,7 @@ export interface JellyfinSettings {
name: string; name: string;
hostname?: string; hostname?: string;
libraries: Library[]; libraries: Library[];
adminUser: string; serverId: string;
adminPass: string;
serverID: string;
} }
interface DVRSettings { interface DVRSettings {
@@ -223,9 +221,7 @@ class Settings {
name: '', name: '',
hostname: '', hostname: '',
libraries: [], libraries: [],
adminUser: '', serverId: '',
adminPass: '',
serverID: '',
}, },
radarr: [], radarr: [],
sonarr: [], sonarr: [],

View File

@@ -43,7 +43,7 @@ authRoutes.post('/plex', async (req, res, next) => {
settings.main.mediaServerType != MediaServerType.PLEX && settings.main.mediaServerType != MediaServerType.PLEX &&
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED 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 { try {
// First we need to use this auth token to get the users email from plex.tv // 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; username?: string;
password?: string; password?: string;
hostname?: string; hostname?: string;
email?: string;
}; };
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured //Make sure jellyfin login is enabled, but only if jellyfin is not already configured
if ( if (
settings.main.mediaServerType != MediaServerType.JELLYFIN && settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
settings.jellyfin.hostname != '' settings.jellyfin.hostname !== ''
) { ) {
return res.status(500).json({ error: 'Jellyfin login is disabled' }); return res.status(500).json({ error: 'Jellyfin login is disabled' });
} else if (!body.username || !body.password) { } else if (!body.username || !body.password) {
return res return res
.status(500) .status(500)
.json({ error: 'You must provide an username and a password' }); .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 return res
.status(500) .status(500)
.json({ error: 'Jellyfin hostname already configured' }); .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.' }); return res.status(500).json({ error: 'No hostname provided.' });
} }
try { try {
const hostname = const hostname =
settings.jellyfin.hostname != '' settings.jellyfin.hostname !== ''
? settings.jellyfin.hostname ? settings.jellyfin.hostname
: body.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 // First we need to attempt to log the user in to jellyfin
const jellyfinserver = new JellyfinAPI(hostname ?? ''); const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId);
settings.jellyfin.name = await jellyfinserver.getServerName();
const account = await jellyfinserver.login(body.username, body.password); const account = await jellyfinserver.login(body.username, body.password);
// Next let's see if the user already exists // Next let's see if the user already exists
let user = await userRepository.findOne({ user = await userRepository.findOne({
where: { jellyfinId: account.User.Id }, where: { jellyfinUserId: account.User.Id },
}); });
if (user) { if (user) {
@@ -200,9 +212,9 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
if (typeof account.User.PrimaryImageTag !== undefined) { if (typeof account.User.PrimaryImageTag !== undefined) {
user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; user.avatar = `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
} else { } else {
user.avatar = '/images/os_logo_square.png'; user.avatar = '/os_logo_square.png';
} }
user.email = account.User.Name;
user.jellyfinUsername = account.User.Name; user.jellyfinUsername = account.User.Name;
if (user.username === 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 // Here we check if it's the first user. If it is, we create the user with no check
// and give them admin permissions // and give them admin permissions
const totalUsers = await userRepository.count(); const totalUsers = await userRepository.count();
if (totalUsers === 0) { if (totalUsers === 0) {
user = new User({ user = new User({
email: account.User.Name, email: body.email,
jellyfinUsername: account.User.Name, jellyfinUsername: account.User.Name,
jellyfinId: account.User.Id, jellyfinUserId: account.User.Id,
jellyfinDeviceId: deviceId,
jellyfinAuthToken: account.AccessToken, jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN, permissions: Permission.ADMIN,
avatar: avatar:
typeof account.User.PrimaryImageTag !== undefined typeof account.User.PrimaryImageTag !== undefined
? `${hostname}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` ? `${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, userType: UserType.JELLYFIN,
}); });
await userRepository.save(user); await userRepository.save(user);
//Update hostname in settings if it doesn't exist (initial configuration) //Update hostname in settings if it doesn't exist (initial configuration)
//Also set mediaservertype to JELLYFIN //Also set mediaservertype to JELLYFIN
if (settings.jellyfin.hostname == '') { if (settings.jellyfin.hostname === '') {
settings.main.mediaServerType = MediaServerType.JELLYFIN; settings.main.mediaServerType = MediaServerType.JELLYFIN;
settings.jellyfin.hostname = body.hostname ?? ''; settings.jellyfin.hostname = body.hostname ?? '';
settings.jellyfin.serverId = account.User.ServerId;
settings.save(); 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 // Set logged in session
@@ -246,16 +280,32 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {}); return res.status(200).json(user?.filter() ?? {});
} catch (e) { } 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' }); logger.error(e.message, { label: 'Auth' });
return next({ return next({
status: 500, status: 500,
message: 'Something went wrong. Is your auth token valid?', message: 'Something went wrong.',
});
} else {
return next({
status: 401,
message: 'CREDENTIAL_ERROR',
}); });
} }
} }

View File

@@ -228,8 +228,7 @@ settingsRoutes.post('/plex/sync', (req, res) => {
settingsRoutes.get('/jellyfin', (_req, res) => { settingsRoutes.get('/jellyfin', (_req, res) => {
const settings = getSettings(); const settings = getSettings();
//DO NOT RETURN ADMIN USER CREDENTIALS!! res.status(200).json(settings.jellyfin);
res.status(200).json(omit(settings.jellyfin, ['adminUser', 'adminPass']));
}); });
settingsRoutes.post('/jellyfin', (req, res) => { settingsRoutes.post('/jellyfin', (req, res) => {
@@ -247,21 +246,19 @@ settingsRoutes.get('/jellyfin/library', async (req, res) => {
if (req.query.sync) { if (req.query.sync) {
const userRepository = getRepository(User); const userRepository = getRepository(User);
const admin = await userRepository.findOneOrFail({ const admin = await userRepository.findOneOrFail({
select: ['id', 'jellyfinAuthToken'], select: ['id', 'jellyfinAuthToken', 'jellyfinDeviceId'],
order: { id: 'ASC' }, order: { id: 'ASC' },
}); });
const jellyfinClient = new JellyfinAPI( const jellyfinClient = new JellyfinAPI(
settings.jellyfin.hostname ?? '', settings.jellyfin.hostname ?? '',
admin.jellyfinAuthToken ?? '' admin.jellyfinAuthToken ?? '',
admin.jellyfinDeviceId ?? ''
); );
const libraries = await jellyfinClient.getLibraries(); const libraries = await jellyfinClient.getLibraries();
const newLibraries: Library[] = libraries const newLibraries: Library[] = libraries.map((library) => {
// Remove libraries that are not movie or show const existing = settings.jellyfin.libraries.find(
.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 (l) => l.id === library.key && l.name === library.title
); );

View File

@@ -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<AddEmailModalProps> = ({
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 (
<Transition
appear
show
enter="transition ease-in-out duration-300 transform opacity-0"
enterFrom="opacity-0"
enterTo="opacuty-100"
leave="transition ease-in-out duration-300 transform opacity-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Formik
initialValues={{
email: '',
}}
validationSchema={EmailSettingsSchema}
onSubmit={async (values) => {
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 (
<Modal
onCancel={onClose}
okButtonType="primary"
okText={
isSubmitting
? intl.formatMessage(messages.saving)
: intl.formatMessage(messages.save)
}
okDisabled={isSubmitting || !isValid}
onOk={() => handleSubmit()}
title={intl.formatMessage(messages.title)}
>
{intl.formatMessage(messages.description, {
applicationName: settings.currentSettings.applicationTitle,
})}
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
placeholder={intl.formatMessage(messages.email)}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
</Modal>
);
}}
</Formik>
</Transition>
);
};
export default AddEmailModal;

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button'; import Button from '../Common/Button';
@@ -7,13 +7,17 @@ import * as Yup from 'yup';
import axios from 'axios'; import axios from 'axios';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSettings from '../../hooks/useSettings'; import useSettings from '../../hooks/useSettings';
import AddEmailModal from './AddEmailModal';
const messages = defineMessages({ const messages = defineMessages({
username: 'Username', username: 'Username',
password: 'Password', password: 'Password',
host: 'Jellyfin URL', host: 'Jellyfin URL',
email: 'Email',
validationhostrequired: 'Jellyfin URL required', validationhostrequired: 'Jellyfin URL required',
validationhostformat: 'Valid URL required', validationhostformat: 'Valid URL required',
validationemailrequired: 'Email required',
validationemailformat: 'Valid email required',
validationusernamerequired: 'Username required', validationusernamerequired: 'Username required',
validationpasswordrequired: 'Password required', validationpasswordrequired: 'Password required',
loginerror: 'Something went wrong while trying to sign in.', loginerror: 'Something went wrong while trying to sign in.',
@@ -34,6 +38,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
revalidate, revalidate,
initial, initial,
}) => { }) => {
const [requiresEmail, setRequiresEmail] = useState<number>(0);
const [username, setUsername] = useState<string>();
const [password, setPassword] = useState<string>();
const toasts = useToasts(); const toasts = useToasts();
const intl = useIntl(); const intl = useIntl();
const settings = useSettings(); const settings = useSettings();
@@ -43,6 +50,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
host: Yup.string() host: Yup.string()
.url(intl.formatMessage(messages.validationhostformat)) .url(intl.formatMessage(messages.validationhostformat))
.required(intl.formatMessage(messages.validationhostrequired)), .required(intl.formatMessage(messages.validationhostrequired)),
email: Yup.string()
.email(intl.formatMessage(messages.validationemailformat))
.required(intl.formatMessage(messages.validationemailrequired)),
username: Yup.string().required( username: Yup.string().required(
intl.formatMessage(messages.validationusernamerequired) intl.formatMessage(messages.validationusernamerequired)
), ),
@@ -56,6 +66,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
username: '', username: '',
password: '', password: '',
host: '', host: '',
email: '',
}} }}
validationSchema={LoginSchema} validationSchema={LoginSchema}
onSubmit={async (values) => { onSubmit={async (values) => {
@@ -64,6 +75,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
username: values.username, username: values.username,
password: values.password, password: values.password,
hostname: values.host, hostname: values.host,
email: values.email,
}); });
} catch (e) { } catch (e) {
toasts.addToast( toasts.addToast(
@@ -101,6 +113,22 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
<div className="error">{errors.host}</div> <div className="error">{errors.host}</div>
)} )}
</div> </div>
<label htmlFor="email" className="text-label">
{intl.formatMessage(messages.email)}
</label>
<div className="mt-1 mb-2 sm:mt-0 sm:col-span-2">
<div className="flex rounded-md shadow-sm">
<Field
id="email"
name="email"
type="text"
placeholder={intl.formatMessage(messages.email)}
/>
</div>
{errors.email && touched.email && (
<div className="error">{errors.email}</div>
)}
</div>
<label htmlFor="username" className="text-label"> <label htmlFor="username" className="text-label">
{intl.formatMessage(messages.username)} {intl.formatMessage(messages.username)}
</label> </label>
@@ -163,6 +191,15 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
), ),
}); });
return ( return (
<div>
{requiresEmail == 1 && (
<AddEmailModal
username={username ?? ''}
password={password ?? ''}
onSave={revalidate}
onClose={() => setRequiresEmail(0)}
></AddEmailModal>
)}
<Formik <Formik
initialValues={{ initialValues={{
username: '', username: '',
@@ -176,6 +213,11 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
password: values.password, password: values.password,
}); });
} catch (e) { } catch (e) {
if (e.message === 'Request failed with status code 406') {
setUsername(values.username);
setPassword(values.password);
setRequiresEmail(1);
} else {
toasts.addToast( toasts.addToast(
intl.formatMessage( intl.formatMessage(
e.message == 'Request failed with status code 401' e.message == 'Request failed with status code 401'
@@ -187,6 +229,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
appearance: 'error', appearance: 'error',
} }
); );
}
} finally { } finally {
revalidate(); revalidate();
} }
@@ -262,6 +305,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
); );
}} }}
</Formik> </Formik>
</div>
); );
} }
}; };

View File

@@ -62,18 +62,18 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
{({ openIndexes, handleClick, AccordionContent }) => ( {({ openIndexes, handleClick, AccordionContent }) => (
<> <>
<button <button
className={`w-full py-2 text-sm text-center hover:bg-gray-700 hover:cursor-pointer text-gray-400 transition-colors duration-200 bg-gray-800 cursor-default focus:outline-none sm:rounded-t-lg ${ className={`w-full py-2 text-sm text-center hover:bg-gray-700 hover:cursor-pointer text-gray-400 transition-colors duration-200 bg-gray-900 cursor-default focus:outline-none sm:rounded-t-lg ${
openIndexes.includes(0) && 'text-indigo-500' openIndexes.includes(0) && 'text-indigo-500'
}`} } ${openIndexes.includes(1) && 'border-b border-gray-500'}`}
onClick={() => handleClick(0)} onClick={() => handleClick(0)}
> >
<FormattedMessage {...messages.signinWithPlex} /> <FormattedMessage {...messages.signinWithPlex} />
</button> </button>
<AccordionContent <AccordionContent isOpen={openIndexes.includes(0)}>
className="bg-opacity-90" <div
isOpen={openIndexes.includes(0)} className="px-10 py-8"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
> >
<div className="px-10 py-8">
<PlexLoginButton <PlexLoginButton
onAuthToken={(authToken) => { onAuthToken={(authToken) => {
setMediaServerType(MediaServerType.PLEX); setMediaServerType(MediaServerType.PLEX);
@@ -84,7 +84,7 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
</AccordionContent> </AccordionContent>
<div> <div>
<button <button
className={`w-full py-2 text-sm text-center text-gray-400 transition-colors duration-200 bg-gray-800 cursor-default focus:outline-none hover:bg-gray-700 hover:cursor-pointer ${ className={`w-full py-2 text-sm text-center text-gray-400 transition-colors duration-200 bg-gray-900 cursor-default focus:outline-none hover:bg-gray-700 hover:cursor-pointer ${
openIndexes.includes(1) openIndexes.includes(1)
? 'text-indigo-500' ? 'text-indigo-500'
: 'sm:rounded-b-lg' : 'sm:rounded-b-lg'
@@ -93,11 +93,11 @@ const SetupLogin: React.FC<LoginWithMediaServerProps> = ({ onComplete }) => {
> >
<FormattedMessage {...messages.signinWithJellyfin} /> <FormattedMessage {...messages.signinWithJellyfin} />
</button> </button>
<AccordionContent <AccordionContent isOpen={openIndexes.includes(1)}>
className="bg-opacity-90" <div
isOpen={openIndexes.includes(1)} className="px-10 py-8 rounded-b-lg"
style={{ backgroundColor: 'rgba(0,0,0,0.3)' }}
> >
<div className="px-10 py-8">
<JellyfinLogin initial={true} revalidate={revalidate} /> <JellyfinLogin initial={true} revalidate={revalidate} />
</div> </div>
</AccordionContent> </AccordionContent>

View File

@@ -104,7 +104,10 @@ const Setup: React.FC = () => {
/> />
</ul> </ul>
</nav> </nav>
<div className="w-full p-4 mt-10 text-white bg-gray-800 bg-opacity-50 border border-gray-600 rounded-md"> <div
style={{ backdropFilter: 'blur(5px)' }}
className="w-full p-4 mt-10 text-white bg-gray-800 border border-gray-600 rounded-md bg-opacity-40"
>
{currentStep === 1 && ( {currentStep === 1 && (
<SetupLogin <SetupLogin
onComplete={() => { onComplete={() => {

View File

@@ -37,6 +37,7 @@
"components.Layout.UserDropdown.signout": "Sign Out", "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.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.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.email": "Email Address",
"components.Login.forgotpassword": "Forgot Password?", "components.Login.forgotpassword": "Forgot Password?",
"components.Login.host": "Jellyfin URL", "components.Login.host": "Jellyfin URL",
@@ -44,13 +45,19 @@
"components.Login.initialsigningin": "Connecting…", "components.Login.initialsigningin": "Connecting…",
"components.Login.loginerror": "Something went wrong while trying to sign in.", "components.Login.loginerror": "Something went wrong while trying to sign in.",
"components.Login.password": "Password", "components.Login.password": "Password",
"components.Login.save": "Add",
"components.Login.saving": "Adding…",
"components.Login.signin": "Sign In", "components.Login.signin": "Sign In",
"components.Login.signingin": "Signing in…", "components.Login.signingin": "Signing in…",
"components.Login.signinheader": "Sign in to continue", "components.Login.signinheader": "Sign in to continue",
"components.Login.signinwithjellyfin": "Use your Jellyfin account", "components.Login.signinwithjellyfin": "Use your Jellyfin account",
"components.Login.signinwithoverseerr": "Use your {applicationTitle} account", "components.Login.signinwithoverseerr": "Use your {applicationTitle} account",
"components.Login.signinwithplex": "Use your Plex account", "components.Login.signinwithplex": "Use your Plex account",
"components.Login.title": "Add Email",
"components.Login.username": "Username", "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.validationemailrequired": "You must provide a valid email address",
"components.Login.validationhostformat": "Valid URL required", "components.Login.validationhostformat": "Valid URL required",
"components.Login.validationhostrequired": "Jellyfin URL required", "components.Login.validationhostrequired": "Jellyfin URL required",
@@ -598,8 +605,8 @@
"components.Setup.setup": "Setup", "components.Setup.setup": "Setup",
"components.Setup.signin": "Sign In", "components.Setup.signin": "Sign In",
"components.Setup.signinMessage": "Get started by signing in", "components.Setup.signinMessage": "Get started by signing in",
"components.Setup.signinWithJellyfin": "Use Jellyfin", "components.Setup.signinWithJellyfin": "Use your Jellyfin account",
"components.Setup.signinWithPlex": "Sign in with Plex", "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.syncingbackground": "Syncing will run in the background. You can continue the setup process in the meantime.",
"components.Setup.tip": "Tip", "components.Setup.tip": "Tip",
"components.Setup.welcome": "Welcome to Overseerr", "components.Setup.welcome": "Welcome to Overseerr",