Refactored Flask error handling setup

This commit is contained in:
CasVT
2025-08-15 16:42:25 +02:00
parent 3847e2fa07
commit b427c2c1d4
4 changed files with 95 additions and 48 deletions

View File

@@ -7,6 +7,54 @@ from backend.base.definitions import (ApiResponse, InvalidUsernameReason,
from backend.base.logging import LOGGER
class LogUnauthMindException(MindException):
"""
MindExceptions that inherit from this one will trigger a log of the
requester's IP address once raised.
"""
# region REST responses
class NotFound(MindException):
@property
def api_response(self) -> ApiResponse:
return {
'code': 404,
'error': self.__class__.__name__,
'result': {}
}
class BadRequest(MindException):
@property
def api_response(self) -> ApiResponse:
return {
'code': 400,
'error': self.__class__.__name__,
'result': {}
}
class MethodNotAllowed(MindException):
@property
def api_response(self) -> ApiResponse:
return {
'code': 405,
'error': self.__class__.__name__,
'result': {}
}
class InternalError(MindException):
@property
def api_response(self) -> ApiResponse:
return {
'code': 500,
'error': self.__class__.__name__,
'result': {}
}
# region Input/Output
class KeyNotFound(MindException):
"A key was not found in the input that is required to be given"
@@ -56,7 +104,7 @@ class InvalidKeyValue(MindException):
# region Auth
class AccessUnauthorized(MindException):
class AccessUnauthorized(LogUnauthMindException):
"The password given is not correct"
def __init__(self) -> None:
@@ -74,7 +122,7 @@ class AccessUnauthorized(MindException):
}
class APIKeyInvalid(MindException):
class APIKeyInvalid(LogUnauthMindException):
"The API key is not correct"
def __init__(self, api_key: str) -> None:
@@ -92,7 +140,7 @@ class APIKeyInvalid(MindException):
}
class APIKeyExpired(MindException):
class APIKeyExpired(LogUnauthMindException):
"The API key has expired"
def __init__(self, api_key: str) -> None:
@@ -129,7 +177,7 @@ class OperationNotAllowed(MindException):
}
class NewAccountsNotAllowed(MindException):
class NewAccountsNotAllowed(LogUnauthMindException):
"It's not allowed to create a new account except for the admin"
def __init__(self) -> None:
@@ -266,7 +314,7 @@ class UsernameInvalid(MindException):
}
class UserNotFound(MindException):
class UserNotFound(LogUnauthMindException):
"The user requested can not be found"
def __init__(

View File

@@ -14,14 +14,16 @@ from secrets import token_bytes
from shutil import copy2, move
from sys import base_exec_prefix, executable, platform, version_info
from threading import current_thread
from typing import Callable, Iterable, List, Sequence, Set, Tuple, Union, cast
from typing import (Any, Callable, Dict, Iterable, List,
Sequence, Set, Tuple, Union, cast)
from apprise import Apprise, LogCapture
from cron_converter import Cron
from dateutil.relativedelta import relativedelta
from backend.base.definitions import (WEEKDAY_NUMBER, GeneralReminderData,
RepeatQuantity, SendResult, T, U)
RepeatQuantity, SendResult,
Serialisable, T, U)
from backend.base.logging import LOGGER
@@ -199,6 +201,14 @@ def current_thread_id() -> int:
return current_thread().native_id or -1
def return_api(
result: Serialisable,
error: Union[str, None] = None,
code: int = 200
) -> Tuple[Dict[str, Any], int]:
return {'error': error, 'result': result}, code
# region Security
def get_hash(salt: bytes, data: str) -> bytes:
"""Hash a string using the supplied salt.

View File

@@ -17,8 +17,12 @@ from waitress.server import create_server
from waitress.task import ThreadedTaskDispatcher as TTD
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from backend.base.definitions import Constants, StartType, StartTypeHandler
from backend.base.helpers import Singleton, folder_path
from backend.base.custom_exceptions import (BadRequest, InternalError,
LogUnauthMindException,
MethodNotAllowed, NotFound)
from backend.base.definitions import (Constants, MindException,
StartType, StartTypeHandler)
from backend.base.helpers import Singleton, folder_path, return_api
from backend.base.logging import LOGGER
from backend.internals.db import DBConnectionManager, close_db
from backend.internals.db_backup_import import revert_db_import
@@ -84,23 +88,34 @@ class Server(metaclass=Singleton):
# Add error handlers
@app.errorhandler(400)
def bad_request(e):
return {'error': "BadRequest", "result": {}}, 400
return return_api(**BadRequest().api_response)
@app.errorhandler(404)
def not_found(e):
if request.path.startswith(
(Constants.API_PREFIX, Constants.ADMIN_PREFIX)
):
return {'error': "NotFound", "result": {}}, 404
return return_api(**NotFound().api_response)
return render("page_not_found.html")
@app.errorhandler(405)
def method_not_allowed(e):
return {'error': "MethodNotAllowed", "result": {}}, 405
return return_api(**MethodNotAllowed().api_response)
@app.errorhandler(500)
def internal_error(e):
return {'error': "InternalError", "result": {}}, 500
return return_api(**InternalError().api_response)
@app.errorhandler(MindException)
def mind_exception(e: MindException):
if isinstance(e, LogUnauthMindException):
ip = request.environ.get(
'HTTP_X_FORWARDED_FOR',
request.remote_addr
)
LOGGER.warning(f'Unauthorised request from {ip}')
return return_api(**e.api_response)
# Add endpoints
app.register_blueprint(ui)

View File

@@ -9,12 +9,11 @@ from typing import Any, Callable, Dict, Tuple, Union
from flask import Response, g, request, send_file
from backend.base.custom_exceptions import (AccessUnauthorized,
APIKeyExpired, APIKeyInvalid,
LogFileNotFound, UserNotFound)
from backend.base.definitions import (ApiKeyEntry, Constants, MindException,
SendResult, Serialisable, StartType)
from backend.base.helpers import folder_path
from backend.base.custom_exceptions import (APIKeyExpired, APIKeyInvalid,
LogFileNotFound)
from backend.base.definitions import (ApiKeyEntry, Constants,
SendResult, StartType)
from backend.base.helpers import folder_path, return_api
from backend.base.logging import LOGGER, get_log_filepath
from backend.features.reminders import Reminders
from backend.features.static_reminders import StaticReminders
@@ -54,14 +53,6 @@ users = Users()
api_key_map: Dict[int, ApiKeyEntry] = {}
def return_api(
result: Serialisable,
error: Union[str, None] = None,
code: int = 200
) -> Tuple[Dict[str, Any], int]:
return {'error': error, 'result': result}, code
def auth() -> None:
"""Checks if the client is logged in.
@@ -120,28 +111,11 @@ def endpoint_wrapper(
def wrapper(*args, **kwargs):
requires_auth = get_api_docs(request).requires_auth
try:
if requires_auth:
auth()
if requires_auth:
auth()
inputs = input_validation()
result = method(inputs, *args, **kwargs)
except MindException as e:
if isinstance(
e,
(APIKeyInvalid, APIKeyExpired,
UserNotFound, AccessUnauthorized)
):
ip = request.environ.get(
'HTTP_X_FORWARDED_FOR',
request.remote_addr
)
LOGGER.warning(f'Unauthorised request from {ip}')
result = return_api(**e.api_response)
return result
inputs = input_validation()
return method(inputs, *args, **kwargs)
wrapper.__name__ = method.__name__
return wrapper