added docstrings

This commit is contained in:
Kyattsukuro 2025-11-13 10:36:09 +01:00
parent b44177ce47
commit e3ff820ec6
3 changed files with 99 additions and 0 deletions

View File

@ -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/<deletion_target>", 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]

View File

@ -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('/<room>', 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('/<room>', 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:

View File

@ -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')