You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/routes/auth.ts

303 lines
8.6 KiB

import { Router } from 'express';
import { getRepository } from 'typeorm';
import PlexTvAPI from '../api/plextv';
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:
'Requested user endpoint without valid authenticated user in session',
});
}
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 res.status(500).json({ error: 'You must provide an auth token' });
}
try {
// First we need to use this auth token to get the users 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.findOne({
where: { plexId: account.id },
});
if (user) {
// Let's check if their Plex token is up-to-date
if (user.plexToken !== body.authToken) {
user.plexToken = body.authToken;
}
// Update the user's avatar with their Plex thumbnail, in case it changed
user.avatar = account.thumb;
user.email = account.email;
user.plexUsername = account.username;
if (user.username === account.username) {
user.username = '';
}
await userRepository.save(user);
} 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) {
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);
}
// Double check that we didn't create the first admin user before running this
if (!user) {
if (!settings.main.newPlexLogin) {
logger.info(
'Failed sign-in attempt from user who has not been imported to Overseerr.',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'Access denied.',
});
}
// If we get to this point, the user does not already exist so we need to create the
// user _assuming_ they have access to the Plex server
const mainUser = await userRepository.findOneOrFail({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
});
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
if (await mainPlexTv.checkUserAccess(account.id)) {
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.info(
'Failed sign-in attempt from user without access to the Plex server.',
{
label: 'Auth',
account: {
...account,
authentication_token: '__REDACTED__',
authToken: '__REDACTED__',
},
}
);
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(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: 'Local user 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.findOne({
select: ['id', 'password'],
where: { email: body.email },
});
const isCorrectCredentials = await user?.passwordMatch(body.password);
// User doesn't exist or credentials are incorrect
if (!isCorrectCredentials) {
logger.info(
'Failed sign-in attempt from user with incorrect credentials.',
{
label: 'Auth',
account: {
ip: req.ip,
email: body.email,
password: '__REDACTED__',
},
}
);
return next({
status: 403,
message: 'Your sign-in credentials are incorrect.',
});
}
// 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 while attempting to authenticate.', {
label: 'Auth',
error: e.message,
});
return next({
status: 500,
message: 'Something went wrong.',
});
}
});
authRoutes.post('/logout', (req, res, next) => {
req.session?.destroy((err) => {
if (err) {
return next({
status: 500,
message: 'Something went wrong while attempting to sign out.',
});
}
return res.status(200).json({ status: 'ok' });
});
});
authRoutes.post('/reset-password', async (req, res) => {
const userRepository = getRepository(User);
const body = req.body as { email?: string };
if (!body.email) {
return res
.status(500)
.json({ error: 'You must provide an email address.' });
}
const user = await userRepository.findOne({
where: { email: body.email },
});
if (user) {
await user.resetPassword();
userRepository.save(user);
logger.info('Successful request made for recovery link.', {
label: 'User Management',
context: { ip: req.ip, email: body.email },
});
} else {
logger.info('Failed request made to reset a password.', {
label: 'User Management',
context: { 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);
try {
if (!req.body.password || req.body.password?.length < 8) {
const message =
'Failed to reset password. Password must be at least 8 characters long.';
logger.info(message, {
label: 'User Management',
context: { ip: req.ip, guid: req.params.guid },
});
return next({ status: 500, message: message });
}
const user = await userRepository.findOne({
where: { resetPasswordGuid: req.params.guid },
});
if (!user) {
throw new Error('Guid invalid.');
}
if (
!user.recoveryLinkExpirationDate ||
user.recoveryLinkExpirationDate <= new Date()
) {
throw new Error('Recovery link expired.');
}
await user.setPassword(req.body.password);
user.recoveryLinkExpirationDate = null;
userRepository.save(user);
logger.info(`Successfully reset password`, {
label: 'User Management',
context: { ip: req.ip, guid: req.params.guid, email: user.email },
});
return res.status(200).json({ status: 'ok' });
} catch (e) {
logger.info(`Failed to reset password. ${e.message}`, {
label: 'User Management',
context: { ip: req.ip, guid: req.params.guid },
});
return res.status(200).json({ status: 'ok' });
}
});
export default authRoutes;