From e3ff820ec637c4829934fb2219115791f5947b19 Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Thu, 13 Nov 2025 10:36:09 +0100 Subject: [PATCH] added docstrings --- simple_chat_api/endpoints/auth.py | 68 +++++++++++++++++++++++++++ simple_chat_api/endpoints/messages.py | 26 ++++++++++ simple_chat_api/main.py | 5 ++ 3 files changed, 99 insertions(+) diff --git a/simple_chat_api/endpoints/auth.py b/simple_chat_api/endpoints/auth.py index d511efa..4284579 100644 --- a/simple_chat_api/endpoints/auth.py +++ b/simple_chat_api/endpoints/auth.py @@ -15,6 +15,14 @@ app = Bottle() def username_by_token(request) -> str | None: + """ + Extract username from JWT token in request cookies. + Returns non on jwt.ExpiredSignatureError and jwt.InvalidTokenError + + :param request: Bottle request object + + :return: username or None + """ token = request.get_cookie("oauth2") if not token: return None @@ -33,6 +41,15 @@ def user_guard(reyection_msg: str = "Requires authentication", allow_anonymous: def user_guard_decorator(fn: callable): @wraps(fn) def wrapper(*args, **kwargs): + """ + Given a bottle request, try authenticating the user by its cookies. + If authentication fails, return a 401 with the given reyection_msg. + Otherwise, call the decorated function with the user as first argument. + + :param args: arguments to pass to decorated function + :param kwargs: keyword arguments to pass to decorated function + :return: decorated function result or 401 rejection + """ username = username_by_token(request) if not username and not allow_anonymous: response.status = 401 @@ -48,6 +65,10 @@ def admin_guard(reyection_msg: str = "Requires admin priveledges"): @wraps(fn) @user_guard(reyection_msg) def wrapper(user, *args, **kwargs): + """ + Check if an authenticated user has admin priveledges. + user_guard is used to ensure authentication. + """ if user.role != "admin": response.status = 401 return dumps({"error": reyection_msg}) @@ -58,6 +79,13 @@ def admin_guard(reyection_msg: str = "Requires admin priveledges"): @app.route("/token", method=["POST"]) def token(): + """ + Login endpoint that returns a JWT token on successful authentication. + Expects JSON body with "user" and "password" fields. + Sets JWT token as cookie "oauth2". + + :return: JSON with JWT token or error message + """ body = request.body.read() try: data = loads(body.decode('utf-8')) @@ -88,6 +116,11 @@ def token(): @app.route("/", method=["GET"]) def get_user(): + """ + Get the username of the authenticated user from the JWT token. + If no valid token is present, returns 401 Unauthorized. + This is a helper endpoint with no real usecase. + """ username = username_by_token(request) if not username: response.status = 401 @@ -99,6 +132,15 @@ def get_user(): @app.route("/add", method=["POST"]) @admin_guard() def add_user(user): + """ + Add a new user to the system. + Expects JSON body with "new_user", "new_password", and optional "new_admin" fields. + Rejected by methodes of admin_guard if the requester is not an admin. + + :param user: authenticated admin user object + + :return: JSON message indicating success or error + """ data = read_keys_from_request(["new_user", "new_password", "new_admin"]) role = "admin" if data.get("new_admin", False) else "user" response.content_type = 'application/json' @@ -113,6 +155,16 @@ def add_user(user): @app.route("/changePassword", method=["POST"]) @user_guard() def change_password(user): + """ + Request password change for the authenticated user. + Rejected by user_guard if the requester is not authenticated. + Rejected whe the old password does not match. + Expects JSON body with "old_password" and "new_password" fields. + + :param user: authenticated user object + + :return: JSON message indicating success or error + """ data = read_keys_from_request(["new_password", "old_password"]) response.content_type = 'application/json' try: @@ -129,6 +181,16 @@ def change_password(user): @app.route("/delete/", method=["POST"]) @user_guard() def delete_user(user, deletion_target: str): + """ + Delete a user from the system. + The authenticated user can delete itself or an admin can delete any user. + Expects the username of the user to delete as a URL parameter. + + :param user: authenticated user object + :param deletion_target: username of the user to delete + + :return: JSON message indicating success or error + """ response.content_type = 'application/json' is_admin = admin_guard()(lambda _: True)() == True # Note: we could just use user.role == "admin" but we @@ -148,6 +210,12 @@ def delete_user(user, deletion_target: str): @app.route("/getAll", method=["GET"]) @admin_guard() def get_all_users(_): + """ + Get a list of all users in the system. + Rejected by admin_guard if the requester is not an admin. + + :return: JSON list of users with their names and roles + """ response.content_type = 'application/json' users = request.db_connector.get_user(None) user_list = [{"name": u.name, "role": u.role} for u in users] diff --git a/simple_chat_api/endpoints/messages.py b/simple_chat_api/endpoints/messages.py index 9abc6b2..82da453 100644 --- a/simple_chat_api/endpoints/messages.py +++ b/simple_chat_api/endpoints/messages.py @@ -8,6 +8,12 @@ from simple_chat_api.utils import read_keys_from_request app = Bottle() def serialize_message(messages: list[Message]) -> str: + """ + Serialize a list of Message objects into a JSON string. + + :param messages: list of Message objects + :return: JSON string representing the messages + """ return dumps({getattr(msg, "id"): {key: getattr(msg, key) for key in msg.__dict__.keys() if key[0] != '_' and key != 'id'} @@ -16,6 +22,16 @@ def serialize_message(messages: list[Message]) -> str: @app.route('/', method=['POST']) @user_guard() def recive_msg(user: User, room: str): + """ + Receive a new message for a specific room. + Rejected by user_guard if the requester is not authenticated. + Expects JSON body with "content" field. + + :param user: authenticated user object + :param room: room to add the message to + + :return: JSON string representing the newly added message + """ msg = read_keys_from_request(keys=["content"]) if not msg: response.status = 400 @@ -27,6 +43,16 @@ def recive_msg(user: User, room: str): @app.route('/', method=['GET']) @user_guard() def return_msgs(user: User, room: str): + """ + Get messages from a specific room. + Rejected by user_guard if the requester is not authenticated. + Accepts optional 'since' query parameter to get messages after a specific timestamp. + + :param user: authenticated user object + :param room: room to get messages from + + :return: JSON string representing the messages from the room + """ since = request.query.get('since', None) if since: try: diff --git a/simple_chat_api/main.py b/simple_chat_api/main.py index 90fc04d..9aaff73 100644 --- a/simple_chat_api/main.py +++ b/simple_chat_api/main.py @@ -15,6 +15,11 @@ app = Bottle() def initialize_app(): + """ + Construct API, add Middleware, etc. + + :return: Bottle app + """ db = DbConnector(config.db_url) @app.hook('before_request')