mirror of
https://github.com/Casvt/MIND.git
synced 2026-04-03 03:00:22 -04:00
Refactored input validation
This commit is contained in:
@@ -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"],
|
||||
|
||||
267
frontend/api.py
267
frontend/api.py
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user