mirror of
https://github.com/sct/overseerr.git
synced 2025-12-26 16:27:17 +01:00
* refactor(jellyfinsettings): abstract jellyfin hostname, updated ui to reflect it, better validation This PR refactors and abstracts jellyfin hostname into, jellyfin ip, jellyfin port, jellyfin useSsl, and jellyfin urlBase. This makes it more consistent with how plex settings are stored as well. In addition, this improves validation as validation can be applied seperately to them instead of as one whole regex doing the work to validate the url. UI was updated to reflect this. BREAKING CHANGE: Jellyfin settings now does not include a hostname. Instead it abstracted it to ip, port, useSsl, and urlBase. However, migration of old settings to new settings should work automatically. * refactor: remove console logs and use getHostname and ApiErrorCodes * fix: store req.body jellyfin settings temporarily and store only if valid This should fix the issue where settings are saved even if the url was invalid. Now the settings will only be saved if the url is valid. Sort of like a test connection. * refactor: clean up commented out code * refactor(i18n): extract translation keys * fix(auth): auth failing with jellyfin login is disabled * fix(settings): jellyfin migrations replacing the rest of the settings * fix(settings): jellyfin hostname should be carried out if hostname exists * fix(settings): merging the wrong settings source * refactor(settings): use migrator for dynamic settings migrations * refactor(settingsmigrator): settings migration handler and the migrations * test(cypress): fix cypress tests failing cypress settings were lacking some of the jobs so when the startJobs() is called when the app starts, it was failing to schedule the jobs where their cron timings were not specified in the cypress settings. Therefore, this commit adds those jobs back. In addition, other setting options were added to keep cypress settings consistent with a normal user. * chore(prettierignore): ignore cypress/config/settings.cypress.json as it does not need prettier * chore(prettier): ran formatter on cypress config to fix format check error format check locally passes on this file. However, it fails during the github actions format check. Therefore, json language features formatter was run instead of prettier to see if that fixes the issue. * test(cypress): add only missing jobs to the cypress settings * ci: attempt at trying to get formatter to pass on cypress config json file * refactor: revert the changes brought to try and fix formatter added back the rest of the cypress settings and removed cypress settings from .prettierignore * refactor(settings): better erorr logging when jellyfin connection test fails in settings page
745 lines
21 KiB
TypeScript
745 lines
21 KiB
TypeScript
import JellyfinAPI from '@server/api/jellyfin';
|
|
import PlexTvAPI from '@server/api/plextv';
|
|
import { ApiErrorCode } from '@server/constants/error';
|
|
import { MediaServerType } from '@server/constants/server';
|
|
import { UserType } from '@server/constants/user';
|
|
import { getRepository } from '@server/datasource';
|
|
import { User } from '@server/entity/User';
|
|
import { startJobs } from '@server/job/schedule';
|
|
import { Permission } from '@server/lib/permissions';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import { isAuthenticated } from '@server/middleware/auth';
|
|
import { ApiError } from '@server/types/error';
|
|
import { getHostname } from '@server/utils/getHostname';
|
|
import * as EmailValidator from 'email-validator';
|
|
import { Router } from 'express';
|
|
import gravatarUrl from 'gravatar-url';
|
|
import net from 'net';
|
|
|
|
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 },
|
|
});
|
|
|
|
// check if email is required in settings and if user has an valid email
|
|
const settings = await getSettings();
|
|
if (
|
|
settings.notifications.agents.email.options.userEmailRequired &&
|
|
!EmailValidator.validate(user.email)
|
|
) {
|
|
user.warnings.push('userEmailRequired');
|
|
logger.warn(`User ${user.username} has no valid email address`);
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
settings.main.mediaServerType = MediaServerType.PLEX;
|
|
settings.save();
|
|
startJobs();
|
|
|
|
await userRepository.save(user);
|
|
} else {
|
|
const mainUser = await userRepository.findOneOrFail({
|
|
select: { id: true, plexToken: true, plexId: true, email: true },
|
|
where: { id: 1 },
|
|
});
|
|
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
|
|
|
if (!account.id) {
|
|
logger.error('Plex ID was missing from Plex.tv response', {
|
|
label: 'API',
|
|
ip: req.ip,
|
|
email: account.email,
|
|
plexUsername: account.username,
|
|
});
|
|
|
|
return next({
|
|
status: 500,
|
|
message: 'Something went wrong. Try again.',
|
|
});
|
|
}
|
|
|
|
if (
|
|
account.id === mainUser.plexId ||
|
|
(account.email === mainUser.email && !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;
|
|
port?: number;
|
|
urlBase?: string;
|
|
useSsl?: boolean;
|
|
email?: string;
|
|
};
|
|
|
|
//Make sure jellyfin login is enabled, but only if jellyfin is not already configured
|
|
if (
|
|
settings.main.mediaServerType !== MediaServerType.JELLYFIN &&
|
|
settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED
|
|
) {
|
|
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.ip !== '' && body.hostname) {
|
|
return res
|
|
.status(500)
|
|
.json({ error: 'Jellyfin hostname already configured' });
|
|
} else if (settings.jellyfin.ip === '' && !body.hostname) {
|
|
return res.status(500).json({ error: 'No hostname provided.' });
|
|
}
|
|
|
|
try {
|
|
const hostname =
|
|
settings.jellyfin.ip !== ''
|
|
? getHostname()
|
|
: getHostname({
|
|
useSsl: body.useSsl,
|
|
ip: body.hostname,
|
|
port: body.port,
|
|
urlBase: body.urlBase,
|
|
});
|
|
|
|
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 ip = req.ip;
|
|
let clientIp;
|
|
|
|
if (ip) {
|
|
if (net.isIPv4(ip)) {
|
|
clientIp = ip;
|
|
} else if (net.isIPv6(ip)) {
|
|
clientIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
|
}
|
|
}
|
|
|
|
const account = await jellyfinserver.login(
|
|
body.username,
|
|
body.password,
|
|
clientIp
|
|
);
|
|
|
|
// Next let's see if the user already exists
|
|
user = await userRepository.findOne({
|
|
where: { jellyfinUserId: account.User.Id },
|
|
});
|
|
|
|
if (!user && !(await userRepository.count())) {
|
|
// Check if user is admin on jellyfin
|
|
if (account.User.Policy.IsAdministrator === false) {
|
|
throw new ApiError(403, ApiErrorCode.NotAdmin);
|
|
}
|
|
|
|
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 doesn't exist, and there are no users in the database, we'll create the user
|
|
// with admin permission
|
|
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
|
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`
|
|
: gravatarUrl(body.email ?? '', { default: 'mm', size: 200 }),
|
|
userType: UserType.JELLYFIN,
|
|
});
|
|
|
|
const serverName = await jellyfinserver.getServerName();
|
|
|
|
settings.jellyfin.name = serverName;
|
|
settings.jellyfin.serverId = account.User.ServerId;
|
|
settings.jellyfin.ip = body.hostname ?? '';
|
|
settings.jellyfin.port = body.port ?? 8096;
|
|
settings.jellyfin.urlBase = body.urlBase ?? '';
|
|
settings.jellyfin.useSsl = body.useSsl ?? false;
|
|
settings.save();
|
|
startJobs();
|
|
|
|
await userRepository.save(user);
|
|
}
|
|
// User already exists, let's update their information
|
|
else if (account.User.Id === user?.jellyfinUserId) {
|
|
logger.info(
|
|
`Found matching ${
|
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
|
? 'Jellyfin'
|
|
: 'Emby'
|
|
} user; updating user with ${
|
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
|
? 'Jellyfin'
|
|
: 'Emby'
|
|
}`,
|
|
{
|
|
label: 'API',
|
|
ip: req.ip,
|
|
jellyfinUsername: account.User.Name,
|
|
}
|
|
);
|
|
// 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 = gravatarUrl(user.email, {
|
|
default: 'mm',
|
|
size: 200,
|
|
});
|
|
}
|
|
user.jellyfinUsername = account.User.Name;
|
|
|
|
if (user.username === account.User.Name) {
|
|
user.username = '';
|
|
}
|
|
|
|
// TODO: If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY
|
|
// if (process.env.JELLYFIN_TYPE === 'emby') {
|
|
// settings.main.mediaServerType = MediaServerType.EMBY;
|
|
// settings.save();
|
|
// }
|
|
|
|
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 if (!user) {
|
|
logger.info(
|
|
'Sign-in attempt from Jellyfin user with access to the media server; creating new Overseerr user',
|
|
{
|
|
label: 'API',
|
|
ip: req.ip,
|
|
jellyfinUsername: account.User.Name,
|
|
}
|
|
);
|
|
|
|
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`
|
|
: gravatarUrl(body.email, { default: 'mm', size: 200 }),
|
|
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) {
|
|
switch (e.errorCode) {
|
|
case ApiErrorCode.InvalidUrl:
|
|
logger.error(
|
|
`The provided ${
|
|
process.env.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin'
|
|
} is invalid or the server is not reachable.`,
|
|
{
|
|
label: 'Auth',
|
|
error: e.errorCode,
|
|
status: e.statusCode,
|
|
hostname: getHostname({
|
|
useSsl: body.useSsl,
|
|
ip: body.hostname,
|
|
port: body.port,
|
|
urlBase: body.urlBase,
|
|
}),
|
|
}
|
|
);
|
|
return next({
|
|
status: e.statusCode,
|
|
message: e.errorCode,
|
|
});
|
|
|
|
case ApiErrorCode.InvalidCredentials:
|
|
logger.warn(
|
|
'Failed login attempt from user with incorrect Jellyfin credentials',
|
|
{
|
|
label: 'Auth',
|
|
account: {
|
|
ip: req.ip,
|
|
email: body.username,
|
|
password: '__REDACTED__',
|
|
},
|
|
}
|
|
);
|
|
return next({
|
|
status: e.statusCode,
|
|
message: e.errorCode,
|
|
});
|
|
|
|
case ApiErrorCode.NotAdmin:
|
|
logger.warn(
|
|
'Failed login attempt from user without admin permissions',
|
|
{
|
|
label: 'Auth',
|
|
account: {
|
|
ip: req.ip,
|
|
email: body.username,
|
|
},
|
|
}
|
|
);
|
|
return next({
|
|
status: e.statusCode,
|
|
message: e.errorCode,
|
|
});
|
|
|
|
default:
|
|
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: true, plexToken: true, plexId: true },
|
|
where: { id: 1 },
|
|
});
|
|
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;
|