Refactored input validation

This commit is contained in:
CasVT
2024-02-25 22:59:23 +01:00
parent 191325c52e
commit 6f1c37b79c
4 changed files with 337 additions and 211 deletions

View File

@@ -279,10 +279,13 @@ class Reminder:
data[k] = v
# Update database
rq = (data["repeat_quantity"].value
if data["repeat_quantity"] is not None else
None)
if repeated_reminder:
next_time = _find_next_time(
data["time"],
RepeatQuantity(data["repeat_quantity"]) if data["repeat_quantity"] else None,
data["repeat_quantity"],
data["repeat_interval"],
weekdays
)
@@ -302,7 +305,7 @@ class Reminder:
data["title"],
data["text"],
next_time,
data["repeat_quantity"],
rq,
data["repeat_interval"],
data["weekdays"],
data["time"],
@@ -327,7 +330,7 @@ class Reminder:
data["title"],
data["text"],
data["time"],
data["repeat_quantity"],
rq,
data["repeat_interval"],
data["weekdays"],
data["color"],

View File

@@ -31,20 +31,20 @@ from frontend.input_validation import (AllowNewAccountsVariable, ColorVariable,
EditNotificationServicesVariable,
EditTimeVariable, EditTitleVariable,
EditURLVariable, LoginTimeResetVariable,
LoginTimeVariable, NewPasswordVariable,
LoginTimeVariable, Method, Methods,
NewPasswordVariable,
NotificationServicesVariable,
PasswordCreateVariable,
PasswordVariable, QueryVariable,
RepeatIntervalVariable,
RepeatQuantityVariable, SortByVariable,
StaticReminderSortByVariable,
TemplateSortByVariable, TextVariable,
TextVariable, TimelessSortByVariable,
TimeVariable, TitleVariable,
URLVariable, UsernameCreateVariable,
UsernameVariable, WeekDaysVariable,
_admin_api_prefix, admin_api,
admin_api_prefix, api, api_docs,
api_prefix, input_validation)
admin_api, admin_api_prefix, api,
api_prefix, get_api_docs,
input_validation)
#===================
# General variables and functions
@@ -120,14 +120,8 @@ def auth() -> None:
def endpoint_wrapper(method: Callable) -> Callable:
def wrapper(*args, **kwargs):
if request.path.startswith(admin_api_prefix):
requires_auth = api_docs[
_admin_api_prefix + request.url_rule.rule.split(admin_api_prefix)[1]
]['requires_auth']
else:
requires_auth = api_docs[
request.url_rule.rule.split(api_prefix)[1]
]['requires_auth']
requires_auth = get_api_docs(request).requires_auth
try:
if requires_auth:
auth()
@@ -159,8 +153,12 @@ def endpoint_wrapper(method: Callable) -> Callable:
@api.route(
'/auth/login',
'Login to a user account',
{'POST': [[UsernameVariable, PasswordVariable]]},
False,
Methods(
post=Method(
vars=[UsernameVariable, PasswordVariable]
)
),
requires_auth=False,
methods=['POST']
)
@endpoint_wrapper
@@ -213,8 +211,12 @@ def api_status():
@api.route(
'/user/add',
'Create a new user account',
{'POST': [[UsernameCreateVariable, PasswordCreateVariable]]},
False,
Methods(
post=Method(
vars=[UsernameCreateVariable, PasswordCreateVariable]
)
),
requires_auth=False,
methods=['POST']
)
@endpoint_wrapper
@@ -225,10 +227,15 @@ def api_add_user(inputs: Dict[str, str]):
@api.route(
'/user',
'Manage a user account',
{'PUT': [[NewPasswordVariable],
'Change the password of the user account'],
'DELETE': [[],
'Delete the user account']},
Methods(
put=Method(
vars=[NewPasswordVariable],
description="Change the password of the user account"
),
delete=Method(
description='Delete the user account'
)
),
methods=['PUT', 'DELETE']
)
@endpoint_wrapper
@@ -250,10 +257,15 @@ def api_manage_user(inputs: Dict[str, str]):
@api.route(
'/notificationservices',
'Manage the notification services',
{'GET': [[],
'Get a list of all notification services'],
'POST': [[TitleVariable, URLVariable],
'Add a notification service']},
Methods(
get=Method(
description='Get a list of all notification services'
),
post=Method(
vars=[TitleVariable, URLVariable],
description='Add a notification service'
)
),
methods=['GET', 'POST']
)
@endpoint_wrapper
@@ -282,7 +294,11 @@ def api_notification_service_available():
@api.route(
'/notificationservices/test',
'Send a test notification using the supplied Apprise URL',
{'POST': [[URLVariable]]},
Methods(
post=Method(
vars=[URLVariable]
)
),
methods=['POST']
)
@endpoint_wrapper
@@ -296,10 +312,15 @@ def api_test_service(inputs: Dict[str, Any]):
@api.route(
'/notificationservices/<int:n_id>',
'Manage a specific notification service',
{'PUT': [[EditTitleVariable, EditURLVariable],
'Edit the notification service'],
'DELETE': [[],
'Delete the notification service']},
Methods(
put=Method(
vars=[EditTitleVariable, EditURLVariable],
description='Edit the notification service'
),
delete=Method(
description='Delete the notification service'
)
),
methods=['GET', 'PUT', 'DELETE']
)
@endpoint_wrapper
@@ -329,15 +350,20 @@ def api_notification_service(inputs: Dict[str, str], n_id: int):
@api.route(
'/reminders',
'Manage the reminders',
{'GET': [[SortByVariable],
'Get a list of all reminders'],
'POST': [[TitleVariable, TimeVariable,
NotificationServicesVariable, TextVariable,
RepeatQuantityVariable, RepeatIntervalVariable,
WeekDaysVariable,
ColorVariable],
'Add a reminder']
},
Methods(
get=Method(
vars=[SortByVariable],
description='Get a list of all reminders'
),
post=Method(
vars=[TitleVariable, TimeVariable,
NotificationServicesVariable, TextVariable,
RepeatQuantityVariable, RepeatIntervalVariable,
WeekDaysVariable,
ColorVariable],
description='Add a reminder'
),
),
methods=['GET', 'POST']
)
@endpoint_wrapper
@@ -362,7 +388,11 @@ def api_reminders_list(inputs: Dict[str, Any]):
@api.route(
'/reminders/search',
'Search through the list of reminders',
{'GET': [[SortByVariable, QueryVariable]]},
Methods(
get=Method(
vars=[SortByVariable, QueryVariable]
)
),
methods=['GET']
)
@endpoint_wrapper
@@ -376,8 +406,12 @@ def api_reminders_query(inputs: Dict[str, str]):
@api.route(
'/reminders/test',
'Test send a reminder draft',
{'POST': [[TitleVariable, NotificationServicesVariable,
TextVariable]]},
Methods(
post=Method(
vars=[TitleVariable, NotificationServicesVariable,
TextVariable]
)
),
methods=['POST']
)
@endpoint_wrapper
@@ -392,14 +426,19 @@ def api_test_reminder(inputs: Dict[str, Any]):
@api.route(
'/reminders/<int:r_id>',
'Manage a specific reminder',
{'PUT': [[EditTitleVariable, EditTimeVariable,
EditNotificationServicesVariable, TextVariable,
RepeatQuantityVariable, RepeatIntervalVariable,
WeekDaysVariable,
ColorVariable],
'Edit the reminder'],
'DELETE': [[],
'Delete the reminder']},
Methods(
put=Method(
vars=[EditTitleVariable, EditTimeVariable,
EditNotificationServicesVariable, TextVariable,
RepeatQuantityVariable, RepeatIntervalVariable,
WeekDaysVariable,
ColorVariable],
description='Edit the reminder'
),
delete=Method(
description='Delete the reminder'
)
),
methods=['GET', 'PUT', 'DELETE']
)
@endpoint_wrapper
@@ -432,11 +471,17 @@ def api_get_reminder(inputs: Dict[str, Any], r_id: int):
@api.route(
'/templates',
'Manage the templates',
{'GET': [[TemplateSortByVariable],
'Get a list of all templates'],
'POST': [[TitleVariable, NotificationServicesVariable,
TextVariable, ColorVariable],
'Add a template']},
Methods(
get=Method(
vars=[TimelessSortByVariable],
description='Get a list of all templates'
),
post=Method(
vars=[TitleVariable, NotificationServicesVariable,
TextVariable, ColorVariable],
description='Add a template'
)
),
methods=['GET', 'POST']
)
@endpoint_wrapper
@@ -457,7 +502,11 @@ def api_get_templates(inputs: Dict[str, Any]):
@api.route(
'/templates/search',
'Search through the list of templates',
{'GET': [[TemplateSortByVariable, QueryVariable]]},
Methods(
get=Method(
vars=[TimelessSortByVariable, QueryVariable]
)
),
methods=['GET']
)
@endpoint_wrapper
@@ -471,11 +520,16 @@ def api_templates_query(inputs: Dict[str, str]):
@api.route(
'/templates/<int:t_id>',
'Manage a specific template',
{'PUT': [[EditTitleVariable, EditNotificationServicesVariable,
TextVariable, ColorVariable],
'Edit the template'],
'DELETE': [[],
'Delete the template']},
Methods(
put=Method(
vars=[EditTitleVariable, EditNotificationServicesVariable,
TextVariable, ColorVariable],
description='Edit the template'
),
delete=Method(
description='Delete the template'
)
),
methods=['GET', 'PUT', 'DELETE']
)
@endpoint_wrapper
@@ -507,11 +561,17 @@ def api_get_template(inputs: Dict[str, Any], t_id: int):
@api.route(
'/staticreminders',
'Manage the static reminders',
{'GET': [[StaticReminderSortByVariable],
'Get a list of all static reminders'],
'POST': [[TitleVariable, NotificationServicesVariable,
TextVariable, ColorVariable],
'Add a static reminder']},
Methods(
get=Method(
vars=[TimelessSortByVariable],
description='Get a list of all static reminders'
),
post=Method(
vars=[TitleVariable, NotificationServicesVariable,
TextVariable, ColorVariable],
description='Add a static reminder'
)
),
methods=['GET', 'POST']
)
@endpoint_wrapper
@@ -532,7 +592,11 @@ def api_static_reminders_list(inputs: Dict[str, Any]):
@api.route(
'/staticreminders/search',
'Search through the list of staticreminders',
{'GET': [[StaticReminderSortByVariable, QueryVariable]]},
Methods(
get=Method(
vars=[TimelessSortByVariable, QueryVariable]
)
),
methods=['GET']
)
@endpoint_wrapper
@@ -546,13 +610,19 @@ def api_static_reminders_query(inputs: Dict[str, str]):
@api.route(
'/staticreminders/<int:s_id>',
'Manage a specific static reminder',
{'POST': [[],
'Trigger the static reminder'],
'PUT': [[EditTitleVariable, EditNotificationServicesVariable,
TextVariable, ColorVariable],
'Edit the static reminder'],
'DELETE': [[],
'Delete the static reminder']},
Methods(
post=Method(
description='Trigger the static reminder'
),
put=Method(
vars=[EditTitleVariable, EditNotificationServicesVariable,
TextVariable, ColorVariable],
description='Edit the static reminder'
),
delete=Method(
description='Delete the static reminder'
)
),
methods=['GET', 'POST', 'PUT', 'DELETE']
)
@endpoint_wrapper
@@ -616,11 +686,16 @@ def api_settings():
@admin_api.route(
'/settings',
'Interact with the admin settings',
{'GET': [[],
'Get the admin settings'],
'PUT': [[AllowNewAccountsVariable, LoginTimeVariable,
LoginTimeResetVariable],
'Edit the admin settings']},
Methods(
get=Method(
description='Get the admin settings'
),
put=Method(
vars=[AllowNewAccountsVariable, LoginTimeVariable,
LoginTimeResetVariable],
description='Edit the admin settings'
)
),
methods=['GET', 'PUT']
)
@endpoint_wrapper
@@ -642,10 +717,15 @@ def api_admin_settings(inputs: Dict[str, Any]):
@admin_api.route(
'/users',
'Get all users or add one',
{'GET': [[],
'Get all users'],
'POST': [[UsernameCreateVariable, PasswordCreateVariable],
'Add a new user']},
Methods(
get=Method(
description='Get all users'
),
post=Method(
vars=[UsernameCreateVariable, PasswordCreateVariable],
description='Add a new user'
)
),
methods=['GET', 'POST']
)
@endpoint_wrapper
@@ -661,10 +741,15 @@ def api_admin_users(inputs: Dict[str, Any]):
@admin_api.route(
'/users/<int:u_id>',
'Manage a specific user',
{'PUT': [[NewPasswordVariable],
'Change the password of the user account'],
'DELETE': [[],
'Delete the user account']},
Methods(
put=Method(
vars=[NewPasswordVariable],
description='Change the password of the user account'
),
delete=Method(
description='Delete the user account'
)
),
methods=['PUT', 'DELETE']
)
@endpoint_wrapper
@@ -685,7 +770,11 @@ def api_admin_user(inputs: Dict[str, Any], u_id: int):
@admin_api.route(
'/database',
'Download the database',
{'GET': [[]]},
Methods(
get=Method(
description="Download the database file"
),
),
methods=['GET']
)
@endpoint_wrapper

View File

@@ -4,9 +4,13 @@
Input validation for the API
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from os.path import splitext
from re import compile
from typing import Any, Callable, Dict, List, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Union
from apprise import Apprise
from flask import Blueprint, request
@@ -19,23 +23,41 @@ from backend.custom_exceptions import (AccessUnauthorized, InvalidKeyValue,
UsernameInvalid, UsernameTaken,
UserNotFound)
from backend.helpers import (RepeatQuantity, SortingMethod,
TimelessSortingMethod)
TimelessSortingMethod, folder_path)
from backend.settings import _format_setting
if TYPE_CHECKING:
from flask import Request
api_prefix = "/api"
_admin_api_prefix = '/admin'
admin_api_prefix = api_prefix + _admin_api_prefix
color_regex = compile(r'#[0-9a-f]{6}')
api_docs: Dict[str, ApiDocEntry] = {}
class DataSource:
DATA = 1
VALUES = 2
FILES = 3
def __init__(self, request: Request) -> None:
self.map = {
self.DATA: request.get_json() if request.data else {},
self.VALUES: request.values,
self.FILES: request.files
}
return
def __getitem__(self, key: DataSource) -> dict:
return self.map[key]
class InputVariable(ABC):
value: Any
@abstractmethod
def __init__(self, value: Any) -> None:
pass
@@ -75,11 +97,51 @@ class InputVariable(ABC):
pass
class DefaultInputVariable(InputVariable):
@dataclass(frozen=True)
class Method:
description: str = ''
vars: List[InputVariable] = field(default_factory=list)
def __bool__(self) -> bool:
return self.vars != []
@dataclass(frozen=True)
class Methods:
get: Method = Method()
post: Method = Method()
put: Method = Method()
delete: Method = Method()
def __getitem__(self, key: str) -> Method:
return getattr(self, key.lower())
def __bool__(self) -> bool:
return bool(self.get or self.post or self.put or self.delete)
@dataclass(frozen=True)
class ApiDocEntry:
endpoint: str
description: str
requires_auth: bool
used_methods: List[str]
methods: Methods
def get_api_docs(request: Request) -> ApiDocEntry:
if request.path.startswith(admin_api_prefix):
url = _admin_api_prefix + request.url_rule.rule.split(admin_api_prefix)[1]
else:
url = request.url_rule.rule.split(api_prefix)[1]
return api_docs[url]
class BaseInputVariable(InputVariable):
source = DataSource.DATA
required = True
default = None
related_exceptions = []
related_exceptions = [KeyNotFound, InvalidKeyValue]
def __init__(self, value: Any) -> None:
self.value = value
@@ -91,31 +153,38 @@ class DefaultInputVariable(InputVariable):
return f'| {self.name} | {"Yes" if self.required else "No"} | {self.description} | N/A |'
class NonRequiredVersion(InputVariable):
class NonRequiredVersion(BaseInputVariable):
required = False
related_exceptions = [InvalidKeyValue]
def __init__(self, value: Any) -> None:
super().__init__(
value
if value is not None else
self.default
)
return
def validate(self) -> bool:
return self.value is None or super().validate()
class UsernameVariable(DefaultInputVariable):
class UsernameVariable(BaseInputVariable):
name = 'username'
description = 'The username of the user account'
related_exceptions = [KeyNotFound, UserNotFound]
class PasswordVariable(DefaultInputVariable):
class PasswordCreateVariable(BaseInputVariable):
name = 'password'
description = 'The password of the user account'
related_exceptions = [KeyNotFound, AccessUnauthorized]
class NewPasswordVariable(PasswordVariable):
name = 'new_password'
description = 'The new password of the user account'
related_exceptions = [KeyNotFound]
class PasswordVariable(PasswordCreateVariable):
related_exceptions = [KeyNotFound, AccessUnauthorized]
class UsernameCreateVariable(UsernameVariable):
related_exceptions = [
KeyNotFound,
@@ -124,44 +193,39 @@ class UsernameCreateVariable(UsernameVariable):
]
class PasswordCreateVariable(PasswordVariable):
class NewPasswordVariable(BaseInputVariable):
name = 'new_password'
description = 'The new password of the user account'
related_exceptions = [KeyNotFound]
class TitleVariable(DefaultInputVariable):
class TitleVariable(BaseInputVariable):
name = 'title'
description = 'The title of the entry'
related_exceptions = [KeyNotFound]
class URLVariable(DefaultInputVariable):
class URLVariable(BaseInputVariable):
name = 'url'
description = 'The Apprise URL of the notification service'
related_exceptions = [KeyNotFound, InvalidKeyValue]
def validate(self) -> bool:
return Apprise().add(self.value)
return super().validate() and Apprise().add(self.value)
class EditTitleVariable(NonRequiredVersion, TitleVariable):
related_exceptions = []
pass
class EditURLVariable(NonRequiredVersion, URLVariable):
related_exceptions = [InvalidKeyValue]
pass
class SortByVariable(DefaultInputVariable):
class SortByVariable(NonRequiredVersion, BaseInputVariable):
name = 'sort_by'
description = 'How to sort the result'
required = False
source = DataSource.VALUES
_options = [k.lower() for k in SortingMethod._member_names_]
default = SortingMethod._member_names_[0].lower()
related_exceptions = [InvalidKeyValue]
def __init__(self, value: str) -> None:
self.value = value
def validate(self) -> bool:
if not self.value in self._options:
@@ -179,7 +243,7 @@ class SortByVariable(DefaultInputVariable):
)
class TemplateSortByVariable(SortByVariable):
class TimelessSortByVariable(SortByVariable):
_options = [k.lower() for k in TimelessSortingMethod._member_names_]
default = TimelessSortingMethod._member_names_[0].lower()
@@ -190,11 +254,8 @@ class TemplateSortByVariable(SortByVariable):
self.value = TimelessSortingMethod[self.value.upper()]
return True
class StaticReminderSortByVariable(TemplateSortByVariable):
pass
class TimeVariable(DefaultInputVariable):
class TimeVariable(BaseInputVariable):
name = 'time'
description = 'The UTC epoch timestamp that the reminder should be sent at'
related_exceptions = [KeyNotFound, InvalidKeyValue, InvalidTime]
@@ -207,10 +268,13 @@ class EditTimeVariable(NonRequiredVersion, TimeVariable):
related_exceptions = [InvalidKeyValue, InvalidTime]
class NotificationServicesVariable(DefaultInputVariable):
class NotificationServicesVariable(BaseInputVariable):
name = 'notification_services'
description = "Array of the id's of the notification services to use to send the notification"
related_exceptions = [KeyNotFound, InvalidKeyValue, NotificationServiceNotFound]
related_exceptions = [
KeyNotFound, InvalidKeyValue,
NotificationServiceNotFound
]
def validate(self) -> bool:
if not isinstance(self.value, list):
@@ -227,7 +291,7 @@ class EditNotificationServicesVariable(NonRequiredVersion, NotificationServicesV
related_exceptions = [InvalidKeyValue, NotificationServiceNotFound]
class TextVariable(NonRequiredVersion, DefaultInputVariable):
class TextVariable(NonRequiredVersion, BaseInputVariable):
name = 'text'
description = 'The body of the entry'
default = ''
@@ -236,13 +300,10 @@ class TextVariable(NonRequiredVersion, DefaultInputVariable):
return isinstance(self.value, str)
class RepeatQuantityVariable(DefaultInputVariable):
class RepeatQuantityVariable(NonRequiredVersion, BaseInputVariable):
name = 'repeat_quantity'
description = 'The quantity of the repeat_interval'
required = False
_options = [m.lower() for m in RepeatQuantity._member_names_]
default = None
related_exceptions = [InvalidKeyValue]
def validate(self) -> bool:
if self.value is None:
@@ -263,12 +324,9 @@ class RepeatQuantityVariable(DefaultInputVariable):
)
class RepeatIntervalVariable(DefaultInputVariable):
class RepeatIntervalVariable(NonRequiredVersion, BaseInputVariable):
name = 'repeat_interval'
description = 'The number of the interval'
required = False
default = None
related_exceptions = [InvalidKeyValue]
def validate(self) -> bool:
return (
@@ -280,12 +338,9 @@ class RepeatIntervalVariable(DefaultInputVariable):
)
class WeekDaysVariable(DefaultInputVariable):
class WeekDaysVariable(NonRequiredVersion, BaseInputVariable):
name = 'weekdays'
description = 'On which days of the weeks to run the reminder'
required = False
default = None
related_exceptions = [InvalidKeyValue]
_options = {0, 1, 2, 3, 4, 5, 6}
def validate(self) -> bool:
@@ -296,26 +351,25 @@ class WeekDaysVariable(DefaultInputVariable):
)
class ColorVariable(DefaultInputVariable):
class ColorVariable(NonRequiredVersion, BaseInputVariable):
name = 'color'
description = 'The hex code of the color of the entry, which is shown in the web-ui'
required = False
default = None
related_exceptions = [InvalidKeyValue]
def validate(self) -> bool:
return self.value is None or color_regex.search(self.value)
super()
return self.value is None or (
isinstance(self.value, str)
and color_regex.search(self.value)
)
class QueryVariable(DefaultInputVariable):
class QueryVariable(BaseInputVariable):
name = 'query'
description = 'The search term'
source = DataSource.VALUES
class AdminSettingsVariable(DefaultInputVariable):
related_exceptions = [KeyNotFound, InvalidKeyValue]
class AdminSettingsVariable(BaseInputVariable):
def validate(self) -> bool:
try:
_format_setting(self.name, self.value)
@@ -352,54 +406,43 @@ def input_validation() -> Union[None, Dict[str, Any]]:
Union[None, Dict[str, Any]]: `None` if the endpoint + method doesn't require input variables.
Otherwise `Dict[str, Any]` with the input variables, checked and formatted.
"""
inputs = {}
result = {}
input_variables: Dict[str, List[Union[List[InputVariable], str]]]
if request.path.startswith(admin_api_prefix):
input_variables = api_docs[
_admin_api_prefix + request.url_rule.rule.split(admin_api_prefix)[1]
]['input_variables']
else:
input_variables = api_docs[
request.url_rule.rule.split(api_prefix)[1]
]['input_variables']
methods = get_api_docs(request).methods
method = methods[request.method]
noted_variables = method.vars
if not input_variables:
return
if not methods:
return None
if input_variables.get(request.method) is None:
return inputs
if not method:
return result
given_variables = {}
given_variables[DataSource.DATA] = request.get_json() if request.data else {}
given_variables[DataSource.VALUES] = request.values
for input_variable in input_variables[request.method]:
given_variables = DataSource(request)
for noted_var in noted_variables:
if (
input_variable.required and
not input_variable.name in given_variables[input_variable.source]
noted_var.required and
not noted_var.name in given_variables[noted_var.source]
):
raise KeyNotFound(input_variable.name)
raise KeyNotFound(noted_var.name)
input_value = given_variables[input_variable.source].get(
input_variable.name,
input_variable.default
)
value: InputVariable = input_variable(input_value)
input_value = given_variables[noted_var.source].get(noted_var.name)
value: InputVariable = noted_var(input_value)
if not value.validate():
raise InvalidKeyValue(input_variable.name, input_value)
raise InvalidKeyValue(noted_var.name, input_value)
inputs[input_variable.name] = value.value
return inputs
result[noted_var.name] = value.value
return result
api_docs: Dict[str, Dict[str, Any]] = {}
class APIBlueprint(Blueprint):
def route(
self,
rule: str,
description: str = '',
input_variables: Dict[str, List[Union[List[InputVariable], str]]] = {},
input_variables: Methods = Methods(),
requires_auth: bool = True,
**options: Any
) -> Callable[[T_route], T_route]:
@@ -411,22 +454,13 @@ class APIBlueprint(Blueprint):
else:
raise NotImplementedError
api_docs[processed_rule] = {
'endpoint': processed_rule,
'description': description,
'requires_auth': requires_auth,
'methods': options['methods'],
'input_variables': {
k: v[0]
for k, v in input_variables.items()
if v and v[0]
},
'method_descriptions': {
k: v[1]
for k, v in input_variables.items()
if v and len(v) == 2 and v[1]
}
}
api_docs[processed_rule] = ApiDocEntry(
endpoint=processed_rule,
description=description,
requires_auth=requires_auth,
used_methods=options['methods'],
methods=input_variables
)
return super().route(rule, **options)

View File

@@ -11,8 +11,8 @@ from typing import Union
from backend.helpers import folder_path
from frontend.api import (NotificationServiceNotFound, ReminderNotFound,
TemplateNotFound, api_docs, api_prefix)
from frontend.input_validation import DataSource
TemplateNotFound)
from frontend.input_validation import DataSource, api_docs, api_prefix
url_var_map = {
'int:n_id': NotificationServiceNotFound,
@@ -41,7 +41,7 @@ Authentication is done using an API key.
To log in, make a POST request to the [`{api_prefix}/auth/login`](#authlogin) endpoint.
You'll receive an API key, which you can then use in your requests to authenticate.
Supply it via the url parameter `api_key`.
This API key is valid for one hour after which the key expires, any further requests return 401 'APIKeyExpired' and you are required to log in again.
This API key is valid for one hour (though the admin can change this duration) after which the key expires, any further requests return 401 'APIKeyExpired' and you are required to log in again.
If no `api_key` is supplied or it is invalid, 401 `APIKeyInvalid` is returned.
For example:
@@ -76,7 +76,7 @@ for rule, data in api_docs.items():
| Requires being logged in | Description |
| ------------------------ | ----------- |
| {'Yes' if data['requires_auth'] else 'No'} | {data['description']} |
| {'Yes' if data.requires_auth else 'No'} | {data.description} |
"""
url_var = rule.replace('<', '>').split('>')
@@ -87,20 +87,20 @@ for rule, data in api_docs.items():
Replace `<{url_var}>` with the ID of the entry. For example: `{rule.replace(f'<{url_var}>', '2')}`.
"""
for method in data['methods']:
result += f"\n??? {method}\n"
for m_name, method in ((m, data.methods[m]) for m in data.used_methods):
result += f"\n??? {m_name}\n"
if method in data['method_descriptions']:
result += f"\n {data['method_descriptions'][method]}\n"
if method.description:
result += f"\n {method.description}\n"
var_types = {
'url': list(var for var in data['input_variables'].get(method, []) if var.source == DataSource.VALUES),
'body': list(var for var in data['input_variables'].get(method, []) if var.source == DataSource.DATA)
'url': [v for v in method.vars if v.source == DataSource.VALUES],
'body': [v for v in method.vars if v.source == DataSource.DATA],
'file': [v for v in method.vars if v.source == DataSource.FILES]
}
for var_type, entries in var_types.items():
if entries:
entries = [e('') for e in entries]
result += f"""
**Parameters ({var_type})**
@@ -108,18 +108,18 @@ Replace `<{url_var}>` with the ID of the entry. For example: `{rule.replace(f'<{
| ---- | -------- | ----------- | -------------- |
"""
for entry in entries:
result += f" {entry}\n"
result += f" {entry('')}\n"
result += f"""
**Returns**
| Code | Error | Description |
| ---- | ----- | ----------- |
| {201 if method == 'POST' else 200}| N/A | Success |
| {201 if m_name == 'POST' else 200} | N/A | Success |
"""
url_exception = [url_var_map[url_var]] if url_var in url_var_map else []
variable_exceptions = [e for v in data['input_variables'].get(method, []) for e in v.related_exceptions]
variable_exceptions = [e for v in method.vars for e in v.related_exceptions]
related_exceptions = sorted(
(make_exception_instance(e) for e in set(variable_exceptions + url_exception)),
key=lambda e: (e.api_response['code'], e.api_response['error'])