diff --git a/server/foxbank_server/apis/__init__.py b/server/foxbank_server/apis/__init__.py index 7d87e11..e1d307b 100644 --- a/server/foxbank_server/apis/__init__.py +++ b/server/foxbank_server/apis/__init__.py @@ -4,8 +4,14 @@ from flask_smorest import Api from .accounts import bp as acc_bp from .login import bp as login_bp +class ApiWithErr(Api): + def handle_http_exception(self, error): + if error.data and error.data['response']: + return error.data['response'] + return super().handle_http_exception(error) + def init_apis(app: Flask): - api = Api(app, spec_kwargs={ + api = ApiWithErr(app, spec_kwargs={ 'title': 'FoxBank', 'version': '1', 'openapi_version': '3.0.0', diff --git a/server/foxbank_server/apis/accounts.py b/server/foxbank_server/apis/accounts.py index 633d455..186c54d 100644 --- a/server/foxbank_server/apis/accounts.py +++ b/server/foxbank_server/apis/accounts.py @@ -40,9 +40,9 @@ def get_valid_account_types(): def get_account_id(account_id: int): account = db_utils.get_account(account_id=account_id) if account is None: - return returns.NOT_FOUND + return returns.abort(returns.NOT_FOUND) if decorators.user_id != db_utils.whose_account(account): - return returns.UNAUTHORIZED + return returns.abort(returns.UNAUTHORIZED) account = account.to_json() return returns.success(account=account) @@ -54,9 +54,9 @@ def get_account_id(account_id: int): def get_account_iban(iban: str): account = db_utils.get_account(iban=iban) if account is None: - return returns.NOT_FOUND + return returns.abort(returns.NOT_FOUND) if decorators.user_id != db_utils.whose_account(account): - return returns.UNAUTHORIZED + return returns.abort(returns.UNAUTHORIZED) account = account.to_json() return returns.success(account=account) @@ -80,9 +80,9 @@ class AccountsList(MethodView): def post(self, currency: str, account_type: str, custom_name: str): """Create account""" if currency not in VALID_CURRENCIES: - abort(HTTPStatus.UNPROCESSABLE_ENTITY) + return returns.abort(returns.invalid_argument('currency')) if account_type not in ACCOUNT_TYPES: - abort(HTTPStatus.UNPROCESSABLE_ENTITY) + return returns.abort(returns.invalid_argument('account_type')) account = Account(-1, '', currency, account_type, custom_name or '') db_utils.insert_account(decorators.user_id, account) diff --git a/server/foxbank_server/apis/login.py b/server/foxbank_server/apis/login.py index fd6e903..97ca243 100644 --- a/server/foxbank_server/apis/login.py +++ b/server/foxbank_server/apis/login.py @@ -29,11 +29,11 @@ class Login(MethodView): """Login via username and TOTP code""" user: User | None = get_user(username=username) if user is None: - return returns.INVALID_DETAILS + return returns.abort(returns.INVALID_DETAILS) otp = TOTP(user.otp) if not otp.verify(code, valid_window=1): - return returns.INVALID_DETAILS + return returns.abort(returns.INVALID_DETAILS) token = ram_db.login_user(user.id) return returns.success(token=token) diff --git a/server/foxbank_server/decorators.py b/server/foxbank_server/decorators.py index 1fa8502..05565b6 100644 --- a/server/foxbank_server/decorators.py +++ b/server/foxbank_server/decorators.py @@ -49,13 +49,13 @@ class Module(ModuleType): def wrapper(*args, **kargs): token = request.headers.get('Authorization', None) if token is None: - return returns.NO_AUTHORIZATION + return returns.abort(returns.NO_AUTHORIZATION) if not token.startswith('Bearer '): - return returns.INVALID_AUTHORIZATION + return returns.abort(returns.INVALID_AUTHORIZATION) token = token[7:] user_id = ram_db.get_user(token) if user_id is None: - return returns.INVALID_AUTHORIZATION + return returns.abort(returns.INVALID_AUTHORIZATION) global _token _token = token @@ -71,40 +71,4 @@ class Module(ModuleType): return wrapper - # def ensure_logged_in(token=False, user_id=False): - # """ - # Ensure the user is logged in by providing an Authorization: Bearer token - # header. - # - # @param token whether the token should be supplied after validation - # @param user_id whether the user_id should be supplied after validation - # @return decorator which supplies the requested parameters - # """ - # def decorator(fn): - # pass_token = token - # pass_user_id = user_id - # - # @wraps(fn) - # def wrapper(*args, **kargs): - # token = request.headers.get('Authorization', None) - # if token is None: - # return returns.NO_AUTHORIZATION - # if not token.startswith('Bearer '): - # return returns.INVALID_AUTHORIZATION - # token = token[7:] - # user_id = ram_db.get_user(token) - # if user_id is None: - # return returns.INVALID_AUTHORIZATION - # - # if pass_user_id and pass_token: - # return fn(user_id=user_id, token=token, *args, **kargs) - # elif pass_user_id: - # return fn(user_id=user_id, *args, **kargs) - # elif pass_token: - # return fn(token=token, *args, **kargs) - # else: - # return fn(*args, **kargs) - # return wrapper - # return decorator - sys.modules[__name__] = Module(__name__) diff --git a/server/foxbank_server/returns.py b/server/foxbank_server/returns.py index cf7e1c5..f78f852 100644 --- a/server/foxbank_server/returns.py +++ b/server/foxbank_server/returns.py @@ -31,6 +31,13 @@ NOT_FOUND = _make_error( 'general/not_found', ) +def invalid_argument(argname: str) -> tuple[Any, int]: + return _make_error( + _HTTPStatus.UNPROCESSABLE_ENTITY, + 'general/invalid_argument', + message=f'Invalid argument: {argname}', + ) + # Login INVALID_DETAILS = _make_error( @@ -76,3 +83,12 @@ class ErrorSchema(Schema): class SuccessSchema(Schema): status = fields.Constant('success') + +# smorest + +def abort(result: tuple[Any, int]): + try: + from flask_smorest import abort as _abort + _abort(result[1], response=result) + except ImportError: + return result diff --git a/server/login.py b/server/login.py deleted file mode 100644 index d462351..0000000 --- a/server/login.py +++ /dev/null @@ -1,47 +0,0 @@ -from functools import wraps -from flask import Blueprint, request - -from pyotp import TOTP - -import db_utils -from decorators import no_content, ensure_logged_in, user_id, token -import models -import ram_db -import returns - -login = Blueprint('login', __name__) - -@login.post('/') -def make_login(): - try: - username = request.json['username'] - code = request.json['code'] - except (TypeError, KeyError): - return returns.INVALID_REQUEST - - user: models.User | None = db_utils.get_user(username=username) - if user is None: - return returns.INVALID_DETAILS - - otp = TOTP(user.otp) - if not otp.verify(code, valid_window=1): - return returns.INVALID_DETAILS - - token = ram_db.login_user(user.id) - return returns.success(token=token) - -@login.post('/logout') -@ensure_logged_in -@no_content -def logout(): - ram_db.logout_user(token) - -@login.get('/whoami') -@ensure_logged_in -def whoami(): - user: models.User | None = db_utils.get_user(user_id=user_id) - if user is not None: - user = user.to_json() - - return returns.successs(user=user) -