fix(logging): handle media server connection refused error/toast (#748)

* fix(logging): handle media server connection refused error/toast

Properly log as connection refused if the jellyfin/emby server is unreachable. Previously it used to
throw a credentials error which lead to a lot of confusion

* refactor(i8n): extract translation keys

* refactor(auth): error message for a more consistent format

* refactor(auth/errors): use custom error types and error codes instead of abusing error messages

* refactor(i8n): replace connection refused translation key with invalidurl

* fix(error): combine auth and api error class into a single one called network error

* fix(error): use the new network error and network error codes in auth/api

* refactor(error): rename NetworkError to ApiError
This commit is contained in:
Fallenbagel
2024-05-23 19:34:31 +05:00
committed by GitHub
parent 10082292e8
commit f486fb5e75
6 changed files with 127 additions and 51 deletions

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ApiErrorCode } from '@server/constants/error';
import availabilitySync from '@server/lib/availabilitySync';
import logger from '@server/logger';
import { ApiError } from '@server/types/error';
import type { AxiosInstance } from 'axios';
import axios from 'axios';
@@ -129,9 +131,33 @@ class JellyfinAPI {
Pw: Password,
}
);
return account.data;
} catch (e) {
throw new Error('Unauthorized');
const status = e.response?.status;
const networkErrorCodes = new Set([
'ECONNREFUSED',
'EHOSTUNREACH',
'ENOTFOUND',
'ETIMEDOUT',
'ECONNRESET',
'EADDRINUSE',
'ENETDOWN',
'ENETUNREACH',
'EPIPE',
'ECONNABORTED',
'EPROTO',
'EHOSTDOWN',
'EAI_AGAIN',
'ERR_INVALID_URL',
]);
if (networkErrorCodes.has(e.code) || status === 404) {
throw new ApiError(status, ApiErrorCode.InvalidUrl);
}
throw new ApiError(status, ApiErrorCode.InvalidCredentials);
}
}

View File

@@ -0,0 +1,5 @@
export enum ApiErrorCode {
InvalidUrl = 'INVALID_URL',
InvalidCredentials = 'INVALID_CREDENTIALS',
NotAdmin = 'NOT_ADMIN',
}

View File

@@ -1,5 +1,6 @@
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';
@@ -9,6 +10,7 @@ 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 * as EmailValidator from 'email-validator';
import { Router } from 'express';
import gravatarUrl from 'gravatar-url';
@@ -278,7 +280,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
if (!user && !(await userRepository.count())) {
// Check if user is admin on jellyfin
if (account.User.Policy.IsAdministrator === false) {
throw new Error('not_admin');
throw new ApiError(403, ApiErrorCode.NotAdmin);
}
logger.info(
@@ -412,43 +414,63 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
return res.status(200).json(user?.filter() ?? {});
} catch (e) {
if (e.message === 'Unauthorized') {
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: 401,
message: 'Unauthorized',
});
} else if (e.message === 'not_admin') {
return next({
status: 403,
message: 'CREDENTIAL_ERROR_NOT_ADMIN',
});
} else if (e.message === 'add_email') {
return next({
status: 406,
message: 'CREDENTIAL_ERROR_ADD_EMAIL',
});
} else if (e.message === 'select_server_type') {
return next({
status: 406,
message: 'CREDENTIAL_ERROR_NO_SERVER_TYPE',
});
} else {
logger.error(e.message, { label: 'Auth' });
return next({
status: 500,
message: 'Something went wrong.',
});
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: body.hostname,
}
);
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.',
});
}
}
});

9
server/types/error.ts Normal file
View File

@@ -0,0 +1,9 @@
import type { ApiErrorCode } from '@server/constants/error';
export class ApiError extends Error {
constructor(public statusCode: number, public errorCode: ApiErrorCode) {
super();
this.name = 'apiError';
}
}