From d600a45559e52234cb16bb49392400155c1b5537 Mon Sep 17 00:00:00 2001 From: CyferShepard Date: Sat, 28 May 2022 08:29:45 +0200 Subject: [PATCH 1/3] feat:Remove Requirement for Jellyfin Passwords --- routes/auth.ts | 608 +++++++++++++++++++++++++ src/components/Login/JellyfinLogin.tsx | 17 +- 2 files changed, 614 insertions(+), 11 deletions(-) create mode 100644 routes/auth.ts diff --git a/routes/auth.ts b/routes/auth.ts new file mode 100644 index 000000000..7c92db627 --- /dev/null +++ b/routes/auth.ts @@ -0,0 +1,608 @@ +import { Router } from 'express'; +import { getRepository } from 'typeorm'; +import JellyfinAPI from '../api/jellyfin'; +import PlexTvAPI from '../api/plextv'; +import { MediaServerType } from '../constants/server'; +import { UserType } from '../constants/user'; +import { User } from '../entity/User'; +import { Permission } from '../lib/permissions'; +import { getSettings } from '../lib/settings'; +import logger from '../logger'; +import { isAuthenticated } from '../middleware/auth'; + +const authRoutes = Router(); + +authRoutes.get('/me', isAuthenticated(), async (req, res) => { + const userRepository = getRepository(User); + if (!req.user) { + return res.status(500).json({ + status: 500, + error: 'Please sign in.', + }); + } + const user = await userRepository.findOneOrFail({ + where: { id: req.user.id }, + }); + + return res.status(200).json(user); +}); + +authRoutes.post('/plex', async (req, res, next) => { + const settings = getSettings(); + const userRepository = getRepository(User); + const body = req.body as { authToken?: string }; + + if (!body.authToken) { + return next({ + status: 500, + message: 'Authentication token required.', + }); + } + + if ( + settings.main.mediaServerType != MediaServerType.PLEX && + settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED + ) { + return res.status(500).json({ error: 'Plex login is disabled' }); + } + try { + // First we need to use this auth token to get the user's email from plex.tv + const plextv = new PlexTvAPI(body.authToken); + const account = await plextv.getUser(); + + // Next let's see if the user already exists + let user = await userRepository + .createQueryBuilder('user') + .where('user.plexId = :id', { id: account.id }) + .orWhere('user.email = :email', { + email: account.email.toLowerCase(), + }) + .getOne(); + + if (!user && !(await userRepository.count())) { + user = new User({ + email: account.email, + plexUsername: account.username, + plexId: account.id, + plexToken: account.authToken, + permissions: Permission.ADMIN, + avatar: account.thumb, + userType: UserType.PLEX, + }); + + await userRepository.save(user); + } else { + const mainUser = await userRepository.findOneOrFail({ + select: ['id', 'plexToken', 'plexId'], + order: { id: 'ASC' }, + }); + const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); + + if ( + account.id === mainUser.plexId || + (await mainPlexTv.checkUserAccess(account.id)) + ) { + if (user) { + if (!user.plexId) { + logger.info( + 'Found matching Plex user; updating user with Plex data', + { + label: 'API', + ip: req.ip, + email: user.email, + userId: user.id, + plexId: account.id, + plexUsername: account.username, + } + ); + } + + user.plexToken = body.authToken; + user.plexId = account.id; + user.avatar = account.thumb; + user.email = account.email; + user.plexUsername = account.username; + user.userType = UserType.PLEX; + + await userRepository.save(user); + } else if (!settings.main.newPlexLogin) { + logger.warn( + 'Failed sign-in attempt by unimported Plex user with access to the media server', + { + label: 'API', + ip: req.ip, + email: account.email, + plexId: account.id, + plexUsername: account.username, + } + ); + return next({ + status: 403, + message: 'Access denied.', + }); + } else { + logger.info( + 'Sign-in attempt from Plex user with access to the media server; creating new Overseerr user', + { + label: 'API', + ip: req.ip, + email: account.email, + plexId: account.id, + plexUsername: account.username, + } + ); + user = new User({ + email: account.email, + plexUsername: account.username, + plexId: account.id, + plexToken: account.authToken, + permissions: settings.main.defaultPermissions, + avatar: account.thumb, + userType: UserType.PLEX, + }); + + await userRepository.save(user); + } + } else { + logger.warn( + 'Failed sign-in attempt by Plex user without access to the media server', + { + label: 'API', + ip: req.ip, + email: account.email, + plexId: account.id, + plexUsername: account.username, + } + ); + return next({ + status: 403, + message: 'Access denied.', + }); + } + } + + // Set logged in session + if (req.session) { + req.session.userId = user.id; + } + + return res.status(200).json(user?.filter() ?? {}); + } catch (e) { + logger.error('Something went wrong authenticating with Plex account', { + label: 'API', + errorMessage: e.message, + ip: req.ip, + }); + return next({ + status: 500, + message: 'Unable to authenticate.', + }); + } +}); + +authRoutes.post('/jellyfin', async (req, res, next) => { + const settings = getSettings(); + const userRepository = getRepository(User); + const body = req.body as { + 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 !== '' + ) { + return res.status(500).json({ error: 'Jellyfin login is disabled' }); + } else if (!body.username) { + return res.status(500).json({ error: 'You must provide an username' }); + } else if (settings.jellyfin.hostname !== '' && body.hostname) { + return res + .status(500) + .json({ error: 'Jellyfin hostname already configured' }); + } 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 + : body.hostname; + const { externalHostname } = getSettings().jellyfin; + + // 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(`BOT_overseerr_${body.username ?? ''}`).toString( + 'base64' + ); + } + // First we need to attempt to log the user in to jellyfin + const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); + const jellyfinHost = + externalHostname && externalHostname.length > 0 + ? externalHostname + : hostname; + + const account = await jellyfinserver.login(body.username, body.password); + // Next let's see if the user already exists + user = await userRepository.findOne({ + where: { jellyfinUserId: account.User.Id }, + }); + + if (user) { + // Let's check if their authtoken is up to date + if (user.jellyfinAuthToken !== account.AccessToken) { + user.jellyfinAuthToken = account.AccessToken; + } + + // Update the users avatar with their jellyfin profile pic (incase it changed) + if (account.User.PrimaryImageTag) { + user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; + } else { + user.avatar = '/os_logo_square.png'; + } + + user.jellyfinUsername = account.User.Name; + + if (user.username === account.User.Name) { + user.username = ''; + } + await userRepository.save(user); + } else if (!settings.main.newPlexLogin) { + logger.warn( + 'Failed sign-in attempt by unimported Jellyfin user with access to the media server', + { + label: 'API', + ip: req.ip, + jellyfinUserId: account.User.Id, + jellyfinUsername: account.User.Name, + } + ); + return next({ + status: 403, + message: 'Access denied.', + }); + } else { + // 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) { + logger.info( + 'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr', + { + label: 'API', + ip: req.ip, + jellyfinUsername: account.User.Name, + } + ); + user = new User({ + email: body.email, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : '/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 === '') { + 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: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : '/os_logo_square.png', + userType: UserType.JELLYFIN, + }); + //initialize Jellyfin/Emby users with local login + const passedExplicitPassword = + body.password && body.password.length > 0; + if (passedExplicitPassword) { + await user.setPassword(body.password ?? ''); + } + await userRepository.save(user); + } + } + + // Set logged in session + if (req.session) { + req.session.userId = user?.id; + } + + return res.status(200).json(user?.filter() ?? {}); + } catch (e) { + 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.', + }); + } + } +}); + +authRoutes.post('/local', async (req, res, next) => { + const settings = getSettings(); + const userRepository = getRepository(User); + const body = req.body as { email?: string; password?: string }; + + if (!settings.main.localLogin) { + return res.status(500).json({ error: 'Password sign-in is disabled.' }); + } else if (!body.email || !body.password) { + return res.status(500).json({ + error: 'You must provide both an email address and a password.', + }); + } + try { + const user = await userRepository + .createQueryBuilder('user') + .select(['user.id', 'user.email', 'user.password', 'user.plexId']) + .where('user.email = :email', { email: body.email.toLowerCase() }) + .getOne(); + + if (!user || !(await user.passwordMatch(body.password))) { + logger.warn('Failed sign-in attempt using invalid Overseerr password', { + label: 'API', + ip: req.ip, + email: body.email, + userId: user?.id, + }); + return next({ + status: 403, + message: 'Access denied.', + }); + } + + const mainUser = await userRepository.findOneOrFail({ + select: ['id', 'plexToken', 'plexId'], + order: { id: 'ASC' }, + }); + const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); + + if (!user.plexId) { + try { + const plexUsersResponse = await mainPlexTv.getUsers(); + const account = plexUsersResponse.MediaContainer.User.find( + (account) => + account.$.email && + account.$.email.toLowerCase() === user.email.toLowerCase() + )?.$; + + if ( + account && + (await mainPlexTv.checkUserAccess(parseInt(account.id))) + ) { + logger.info( + 'Found matching Plex user; updating user with Plex data', + { + label: 'API', + ip: req.ip, + email: body.email, + userId: user.id, + plexId: account.id, + plexUsername: account.username, + } + ); + + user.plexId = parseInt(account.id); + user.avatar = account.thumb; + user.email = account.email; + user.plexUsername = account.username; + user.userType = UserType.PLEX; + + await userRepository.save(user); + } + } catch (e) { + logger.error('Something went wrong fetching Plex users', { + label: 'API', + errorMessage: e.message, + }); + } + } + + if ( + user.plexId && + user.plexId !== mainUser.plexId && + !(await mainPlexTv.checkUserAccess(user.plexId)) + ) { + logger.warn( + 'Failed sign-in attempt from Plex user without access to the media server', + { + label: 'API', + account: { + ip: req.ip, + email: body.email, + userId: user.id, + plexId: user.plexId, + }, + } + ); + return next({ + status: 403, + message: 'Access denied.', + }); + } + + // Set logged in session + if (user && req.session) { + req.session.userId = user.id; + } + + return res.status(200).json(user?.filter() ?? {}); + } catch (e) { + logger.error( + 'Something went wrong authenticating with Overseerr password', + { + label: 'API', + errorMessage: e.message, + ip: req.ip, + email: body.email, + } + ); + return next({ + status: 500, + message: 'Unable to authenticate.', + }); + } +}); + +authRoutes.post('/logout', (req, res, next) => { + req.session?.destroy((err) => { + if (err) { + return next({ + status: 500, + message: 'Something went wrong.', + }); + } + + return res.status(200).json({ status: 'ok' }); + }); +}); + +authRoutes.post('/reset-password', async (req, res, next) => { + const userRepository = getRepository(User); + const body = req.body as { email?: string }; + + if (!body.email) { + return next({ + status: 500, + message: 'Email address required.', + }); + } + + const user = await userRepository + .createQueryBuilder('user') + .where('user.email = :email', { email: body.email.toLowerCase() }) + .getOne(); + + if (user) { + await user.resetPassword(); + userRepository.save(user); + logger.info('Successfully sent password reset link', { + label: 'API', + ip: req.ip, + email: body.email, + }); + } else { + logger.error('Something went wrong sending password reset link', { + label: 'API', + ip: req.ip, + email: body.email, + }); + } + + return res.status(200).json({ status: 'ok' }); +}); + +authRoutes.post('/reset-password/:guid', async (req, res, next) => { + const userRepository = getRepository(User); + + if (!req.body.password || req.body.password?.length < 8) { + logger.warn('Failed password reset attempt using invalid new password', { + label: 'API', + ip: req.ip, + guid: req.params.guid, + }); + return next({ + status: 500, + message: 'Password must be at least 8 characters long.', + }); + } + + const user = await userRepository.findOne({ + where: { resetPasswordGuid: req.params.guid }, + }); + + if (!user) { + logger.warn('Failed password reset attempt using invalid recovery link', { + label: 'API', + ip: req.ip, + guid: req.params.guid, + }); + return next({ + status: 500, + message: 'Invalid password reset link.', + }); + } + + if ( + !user.recoveryLinkExpirationDate || + user.recoveryLinkExpirationDate <= new Date() + ) { + logger.warn('Failed password reset attempt using expired recovery link', { + label: 'API', + ip: req.ip, + guid: req.params.guid, + email: user.email, + }); + return next({ + status: 500, + message: 'Invalid password reset link.', + }); + } + user.recoveryLinkExpirationDate = null; + userRepository.save(user); + logger.info('Successfully reset password', { + label: 'API', + ip: req.ip, + guid: req.params.guid, + email: user.email, + }); + + return res.status(200).json({ status: 'ok' }); +}); + +export default authRoutes; diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index c93c9268d..a6cb0c0c1 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -1,12 +1,11 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import Button from '../Common/Button'; - -import { Field, Form, Formik } from 'formik'; -import * as Yup from 'yup'; -import axios from 'axios'; import { useToasts } from 'react-toast-notifications'; +import * as Yup from 'yup'; import useSettings from '../../hooks/useSettings'; +import Button from '../Common/Button'; const messages = defineMessages({ username: 'Username', @@ -63,9 +62,7 @@ const JellyfinLogin: React.FC = ({ username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), - password: Yup.string().required( - intl.formatMessage(messages.validationpasswordrequired) - ), + password: Yup.string(), }); return ( = ({ username: Yup.string().required( intl.formatMessage(messages.validationusernamerequired) ), - password: Yup.string().required( - intl.formatMessage(messages.validationpasswordrequired) - ), + password: Yup.string(), }); return (
From bda7858b66dc7023fb417f924c45cf24fdc28a2a Mon Sep 17 00:00:00 2001 From: CyferShepard Date: Sat, 28 May 2022 08:50:23 +0200 Subject: [PATCH 2/3] Delete auth.ts --- server/routes/auth.ts | 610 ------------------------------------------ 1 file changed, 610 deletions(-) delete mode 100644 server/routes/auth.ts diff --git a/server/routes/auth.ts b/server/routes/auth.ts deleted file mode 100644 index 69d2fd52c..000000000 --- a/server/routes/auth.ts +++ /dev/null @@ -1,610 +0,0 @@ -import { Router } from 'express'; -import { getRepository } from 'typeorm'; -import JellyfinAPI from '../api/jellyfin'; -import PlexTvAPI from '../api/plextv'; -import { MediaServerType } from '../constants/server'; -import { UserType } from '../constants/user'; -import { User } from '../entity/User'; -import { Permission } from '../lib/permissions'; -import { getSettings } from '../lib/settings'; -import logger from '../logger'; -import { isAuthenticated } from '../middleware/auth'; - -const authRoutes = Router(); - -authRoutes.get('/me', isAuthenticated(), async (req, res) => { - const userRepository = getRepository(User); - if (!req.user) { - return res.status(500).json({ - status: 500, - error: 'Please sign in.', - }); - } - const user = await userRepository.findOneOrFail({ - where: { id: req.user.id }, - }); - - return res.status(200).json(user); -}); - -authRoutes.post('/plex', async (req, res, next) => { - const settings = getSettings(); - const userRepository = getRepository(User); - const body = req.body as { authToken?: string }; - - if (!body.authToken) { - return next({ - status: 500, - message: 'Authentication token required.', - }); - } - - if ( - settings.main.mediaServerType != MediaServerType.PLEX && - settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED - ) { - return res.status(500).json({ error: 'Plex login is disabled' }); - } - try { - // First we need to use this auth token to get the user's email from plex.tv - const plextv = new PlexTvAPI(body.authToken); - const account = await plextv.getUser(); - - // Next let's see if the user already exists - let user = await userRepository - .createQueryBuilder('user') - .where('user.plexId = :id', { id: account.id }) - .orWhere('user.email = :email', { - email: account.email.toLowerCase(), - }) - .getOne(); - - if (!user && !(await userRepository.count())) { - user = new User({ - email: account.email, - plexUsername: account.username, - plexId: account.id, - plexToken: account.authToken, - permissions: Permission.ADMIN, - avatar: account.thumb, - userType: UserType.PLEX, - }); - - await userRepository.save(user); - } else { - const mainUser = await userRepository.findOneOrFail({ - select: ['id', 'plexToken', 'plexId'], - order: { id: 'ASC' }, - }); - const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); - - if ( - account.id === mainUser.plexId || - (await mainPlexTv.checkUserAccess(account.id)) - ) { - if (user) { - if (!user.plexId) { - logger.info( - 'Found matching Plex user; updating user with Plex data', - { - label: 'API', - ip: req.ip, - email: user.email, - userId: user.id, - plexId: account.id, - plexUsername: account.username, - } - ); - } - - user.plexToken = body.authToken; - user.plexId = account.id; - user.avatar = account.thumb; - user.email = account.email; - user.plexUsername = account.username; - user.userType = UserType.PLEX; - - await userRepository.save(user); - } else if (!settings.main.newPlexLogin) { - logger.warn( - 'Failed sign-in attempt by unimported Plex user with access to the media server', - { - label: 'API', - ip: req.ip, - email: account.email, - plexId: account.id, - plexUsername: account.username, - } - ); - return next({ - status: 403, - message: 'Access denied.', - }); - } else { - logger.info( - 'Sign-in attempt from Plex user with access to the media server; creating new Overseerr user', - { - label: 'API', - ip: req.ip, - email: account.email, - plexId: account.id, - plexUsername: account.username, - } - ); - user = new User({ - email: account.email, - plexUsername: account.username, - plexId: account.id, - plexToken: account.authToken, - permissions: settings.main.defaultPermissions, - avatar: account.thumb, - userType: UserType.PLEX, - }); - - await userRepository.save(user); - } - } else { - logger.warn( - 'Failed sign-in attempt by Plex user without access to the media server', - { - label: 'API', - ip: req.ip, - email: account.email, - plexId: account.id, - plexUsername: account.username, - } - ); - return next({ - status: 403, - message: 'Access denied.', - }); - } - } - - // Set logged in session - if (req.session) { - req.session.userId = user.id; - } - - return res.status(200).json(user?.filter() ?? {}); - } catch (e) { - logger.error('Something went wrong authenticating with Plex account', { - label: 'API', - errorMessage: e.message, - ip: req.ip, - }); - return next({ - status: 500, - message: 'Unable to authenticate.', - }); - } -}); - -authRoutes.post('/jellyfin', async (req, res, next) => { - const settings = getSettings(); - const userRepository = getRepository(User); - const body = req.body as { - 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 !== '' - ) { - 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) { - return res - .status(500) - .json({ error: 'Jellyfin hostname already configured' }); - } 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 - : body.hostname; - const { externalHostname } = getSettings().jellyfin; - - // 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(`BOT_overseerr_${body.username ?? ''}`).toString( - 'base64' - ); - } - // First we need to attempt to log the user in to jellyfin - const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); - const jellyfinHost = - externalHostname && externalHostname.length > 0 - ? externalHostname - : hostname; - - const account = await jellyfinserver.login(body.username, body.password); - // Next let's see if the user already exists - user = await userRepository.findOne({ - where: { jellyfinUserId: account.User.Id }, - }); - - if (user) { - // Let's check if their authtoken is up to date - if (user.jellyfinAuthToken !== account.AccessToken) { - user.jellyfinAuthToken = account.AccessToken; - } - - // Update the users avatar with their jellyfin profile pic (incase it changed) - if (account.User.PrimaryImageTag) { - user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`; - } else { - user.avatar = '/os_logo_square.png'; - } - - user.jellyfinUsername = account.User.Name; - - if (user.username === account.User.Name) { - user.username = ''; - } - await userRepository.save(user); - } else if (!settings.main.newPlexLogin) { - logger.warn( - 'Failed sign-in attempt by unimported Jellyfin user with access to the media server', - { - label: 'API', - ip: req.ip, - jellyfinUserId: account.User.Id, - jellyfinUsername: account.User.Name, - } - ); - return next({ - status: 403, - message: 'Access denied.', - }); - } else { - // 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) { - logger.info( - 'Sign-in attempt from Jellyfin user with access to the media server; creating initial admin user for Overseerr', - { - label: 'API', - ip: req.ip, - jellyfinUsername: account.User.Name, - } - ); - user = new User({ - email: body.email, - jellyfinUsername: account.User.Name, - jellyfinUserId: account.User.Id, - jellyfinDeviceId: deviceId, - jellyfinAuthToken: account.AccessToken, - permissions: Permission.ADMIN, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : '/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 === '') { - 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: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : '/os_logo_square.png', - userType: UserType.JELLYFIN, - }); - //initialize Jellyfin/Emby users with local login - const passedExplicitPassword = - body.password && body.password.length > 0; - if (passedExplicitPassword) { - await user.setPassword(body.password); - } - await userRepository.save(user); - } - } - - // Set logged in session - if (req.session) { - req.session.userId = user?.id; - } - - return res.status(200).json(user?.filter() ?? {}); - } catch (e) { - 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.', - }); - } - } -}); - -authRoutes.post('/local', async (req, res, next) => { - const settings = getSettings(); - const userRepository = getRepository(User); - const body = req.body as { email?: string; password?: string }; - - if (!settings.main.localLogin) { - return res.status(500).json({ error: 'Password sign-in is disabled.' }); - } else if (!body.email || !body.password) { - return res.status(500).json({ - error: 'You must provide both an email address and a password.', - }); - } - try { - const user = await userRepository - .createQueryBuilder('user') - .select(['user.id', 'user.email', 'user.password', 'user.plexId']) - .where('user.email = :email', { email: body.email.toLowerCase() }) - .getOne(); - - if (!user || !(await user.passwordMatch(body.password))) { - logger.warn('Failed sign-in attempt using invalid Overseerr password', { - label: 'API', - ip: req.ip, - email: body.email, - userId: user?.id, - }); - return next({ - status: 403, - message: 'Access denied.', - }); - } - - const mainUser = await userRepository.findOneOrFail({ - select: ['id', 'plexToken', 'plexId'], - order: { id: 'ASC' }, - }); - const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? ''); - - if (!user.plexId) { - try { - const plexUsersResponse = await mainPlexTv.getUsers(); - const account = plexUsersResponse.MediaContainer.User.find( - (account) => - account.$.email && - account.$.email.toLowerCase() === user.email.toLowerCase() - )?.$; - - if ( - account && - (await mainPlexTv.checkUserAccess(parseInt(account.id))) - ) { - logger.info( - 'Found matching Plex user; updating user with Plex data', - { - label: 'API', - ip: req.ip, - email: body.email, - userId: user.id, - plexId: account.id, - plexUsername: account.username, - } - ); - - user.plexId = parseInt(account.id); - user.avatar = account.thumb; - user.email = account.email; - user.plexUsername = account.username; - user.userType = UserType.PLEX; - - await userRepository.save(user); - } - } catch (e) { - logger.error('Something went wrong fetching Plex users', { - label: 'API', - errorMessage: e.message, - }); - } - } - - if ( - user.plexId && - user.plexId !== mainUser.plexId && - !(await mainPlexTv.checkUserAccess(user.plexId)) - ) { - logger.warn( - 'Failed sign-in attempt from Plex user without access to the media server', - { - label: 'API', - account: { - ip: req.ip, - email: body.email, - userId: user.id, - plexId: user.plexId, - }, - } - ); - return next({ - status: 403, - message: 'Access denied.', - }); - } - - // Set logged in session - if (user && req.session) { - req.session.userId = user.id; - } - - return res.status(200).json(user?.filter() ?? {}); - } catch (e) { - logger.error( - 'Something went wrong authenticating with Overseerr password', - { - label: 'API', - errorMessage: e.message, - ip: req.ip, - email: body.email, - } - ); - return next({ - status: 500, - message: 'Unable to authenticate.', - }); - } -}); - -authRoutes.post('/logout', (req, res, next) => { - req.session?.destroy((err) => { - if (err) { - return next({ - status: 500, - message: 'Something went wrong.', - }); - } - - return res.status(200).json({ status: 'ok' }); - }); -}); - -authRoutes.post('/reset-password', async (req, res, next) => { - const userRepository = getRepository(User); - const body = req.body as { email?: string }; - - if (!body.email) { - return next({ - status: 500, - message: 'Email address required.', - }); - } - - const user = await userRepository - .createQueryBuilder('user') - .where('user.email = :email', { email: body.email.toLowerCase() }) - .getOne(); - - if (user) { - await user.resetPassword(); - userRepository.save(user); - logger.info('Successfully sent password reset link', { - label: 'API', - ip: req.ip, - email: body.email, - }); - } else { - logger.error('Something went wrong sending password reset link', { - label: 'API', - ip: req.ip, - email: body.email, - }); - } - - return res.status(200).json({ status: 'ok' }); -}); - -authRoutes.post('/reset-password/:guid', async (req, res, next) => { - const userRepository = getRepository(User); - - if (!req.body.password || req.body.password?.length < 8) { - logger.warn('Failed password reset attempt using invalid new password', { - label: 'API', - ip: req.ip, - guid: req.params.guid, - }); - return next({ - status: 500, - message: 'Password must be at least 8 characters long.', - }); - } - - const user = await userRepository.findOne({ - where: { resetPasswordGuid: req.params.guid }, - }); - - if (!user) { - logger.warn('Failed password reset attempt using invalid recovery link', { - label: 'API', - ip: req.ip, - guid: req.params.guid, - }); - return next({ - status: 500, - message: 'Invalid password reset link.', - }); - } - - if ( - !user.recoveryLinkExpirationDate || - user.recoveryLinkExpirationDate <= new Date() - ) { - logger.warn('Failed password reset attempt using expired recovery link', { - label: 'API', - ip: req.ip, - guid: req.params.guid, - email: user.email, - }); - return next({ - status: 500, - message: 'Invalid password reset link.', - }); - } - user.recoveryLinkExpirationDate = null; - userRepository.save(user); - logger.info('Successfully reset password', { - label: 'API', - ip: req.ip, - guid: req.params.guid, - email: user.email, - }); - - return res.status(200).json({ status: 'ok' }); -}); - -export default authRoutes; From ad7b3590d78b346c9fe0624eb9cb90e759e8fb3d Mon Sep 17 00:00:00 2001 From: CyferShepard Date: Sat, 28 May 2022 08:50:51 +0200 Subject: [PATCH 3/3] Move auth.ts to correct folder --- {routes => server/routes}/auth.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {routes => server/routes}/auth.ts (100%) diff --git a/routes/auth.ts b/server/routes/auth.ts similarity index 100% rename from routes/auth.ts rename to server/routes/auth.ts