From a1273aa39f117989777c45549f012e42cf2fc404 Mon Sep 17 00:00:00 2001 From: Raine Date: Sun, 7 Jan 2024 03:28:47 +0100 Subject: [PATCH] init: initial commit --- .gitignore | 3 ++ README.md | 7 ++++ doc/env.md | 7 ++++ requirements.txt | 2 ++ src/AppMeta.py | 25 +++++++++++++ src/Bunq.py | 61 ++++++++++++++++++++++++++++++++ src/main.py | 31 ++++++++++++++++ src/middleware/AuthMiddleware.py | 24 +++++++++++++ 8 files changed, 160 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 doc/env.md create mode 100644 requirements.txt create mode 100644 src/AppMeta.py create mode 100644 src/Bunq.py create mode 100644 src/main.py create mode 100644 src/middleware/AuthMiddleware.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0026404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.env +*.bunq_ctx +venv/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe06024 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# `finmgr` + +Single user finance manager to make sure *someone* doesn't spend too much money. + +## Configure + +check `./doc/env.md` and you'll figure it out! \ No newline at end of file diff --git a/doc/env.md b/doc/env.md new file mode 100644 index 0000000..07d6b9b --- /dev/null +++ b/doc/env.md @@ -0,0 +1,7 @@ +| key | description | required | default | +|-------------------------|-----------------------------------------------------------|----------|-----------| +| `AUTH_USERNAME` | username for basic http auth | no | `admin` | +| `AUTH_PASSWORD` | password for basic http auth | no | `admin` | +| `BUNQ_API_KEY` | Bunq api key | yes | nothing | +| `BUNQ_ENVIRONMENT_TYPE` | Bunq environment type | no | `sandbox` | +| `BUNQ_CONTEXT_PREFIX` | Prefix the path of where you want/have your bunq contexts | no | `.` | diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..914d5c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +bunq_sdk +falcon \ No newline at end of file diff --git a/src/AppMeta.py b/src/AppMeta.py new file mode 100644 index 0000000..8b9f870 --- /dev/null +++ b/src/AppMeta.py @@ -0,0 +1,25 @@ +import socket + +APP_NAME = "finmgr" +APP_VERSION = "1.0" +APP_AUTHORS = [ + "Raine " +] + + +def app_authors_as_string(): + if len(APP_AUTHORS) == 0: + return "No authors" + elif len(APP_AUTHORS) == 1: + return APP_AUTHORS[0] + else: + authors = APP_AUTHORS[:-1] + return ", ".join(authors) + " and " + APP_AUTHORS[-1] + + +def app_identifier(): + return f"{APP_NAME} {APP_VERSION} by {app_authors_as_string()}" + + +def unique_app_identifier(): + return f"{app_identifier()} ({socket.gethostname()})" diff --git a/src/Bunq.py b/src/Bunq.py new file mode 100644 index 0000000..7e2a047 --- /dev/null +++ b/src/Bunq.py @@ -0,0 +1,61 @@ +import enum +import os +from os.path import isfile +import socket + +from bunq import ApiEnvironmentType +from bunq.sdk.context.api_context import ApiContext +from bunq.sdk.context.bunq_context import BunqContext +from bunq.sdk.model.generated.endpoint import User, UserLight, UserCompany, UserPerson + +import AppMeta + + +class Bunq: + def __init__(self, environment_type: ApiEnvironmentType, api_key: str): + self.environment_type = environment_type + self.api_key: str = api_key + self.user = None + self.setup_context() + self.setup_current_user() + + def context_file_path(self): + suffix = "" + if self.environment_type == ApiEnvironmentType.SANDBOX: + suffix = ".sandbox.bunq_ctx" + elif self.environment_type == ApiEnvironmentType.PRODUCTION: + suffix = ".production.bunq_ctx" + return os.path.join(os.environ.get('BUNQ_CONTEXT_PREFIX') or ".", suffix) + + def setup_context(self): + if isfile(self.context_file_path()): + pass # Config is already present + else: + ApiContext.create( + self.environment_type, + self.api_key, + AppMeta.unique_app_identifier() + ).save(self.context_file_path()) + + api_context = ApiContext.restore(self.context_file_path()) + api_context.ensure_session_active() + api_context.save(self.context_file_path()) + + BunqContext.load_api_context(api_context) + + def setup_current_user(self): + user = User.get().value.get_referenced_object() + if (isinstance(user, UserPerson) + or isinstance(user, UserCompany) + or isinstance(user, UserLight) + ): + self.user = user + + def current_user(self): + """ + :rtype: UserCompany|UserPerson + """ + return self.user + + def update_context(self): + BunqContext.api_context().save(self.context_file_path()) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..2e39cf3 --- /dev/null +++ b/src/main.py @@ -0,0 +1,31 @@ +import os +from wsgiref.simple_server import make_server + +import falcon +from dotenv import load_dotenv + +from bunq import ApiEnvironmentType + +from Bunq import Bunq +from middleware.AuthMiddleware import AuthMiddleware + +load_dotenv() + +BUNQ = Bunq( + ApiEnvironmentType.PRODUCTION if os.environ.get("BUNQ_ENVIRONMENT_TYPE") == "production" else ApiEnvironmentType.SANDBOX, + os.environ.get("BUNQ_API_KEY")) + + +def main(): + print(f"Running for {BUNQ.current_user().display_name}...") + + app = falcon.App() + app.add_middleware(AuthMiddleware()) + + with make_server('', 8080, app) as server: + print(f"Serving on :8080!") + server.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/src/middleware/AuthMiddleware.py b/src/middleware/AuthMiddleware.py new file mode 100644 index 0000000..3090605 --- /dev/null +++ b/src/middleware/AuthMiddleware.py @@ -0,0 +1,24 @@ +import base64 +import os + +import falcon +from falcon import Request, Response + + +class AuthMiddleware: + def validate_credentials(self, auth_header_value: str) -> bool: + auth_token = auth_header_value.split(' ')[1] + username, password = base64.b64decode(auth_token).decode().split(':') + return username == (os.environ.get('AUTH_USERNAME') or 'admin') and password == (os.environ.get('AUTH_PASSWORD') or 'admin') + + def set_response_to_auth(self, resp: Response): + resp.complete = True + resp.status = 401 + resp.body = "Not authenticated." + resp.set_header("WWW-Authenticate", "Basic realm='Login required'") + + def process_request(self, request: Request, resp: Response): + if request.auth is not None and self.validate_credentials(request.auth): + pass + else: + self.set_response_to_auth(resp) \ No newline at end of file