From 0ff566176596b82657936f6f031b105efba907b2 Mon Sep 17 00:00:00 2001 From: mungai-njoroge Date: Sat, 27 Apr 2024 10:05:15 +0300 Subject: [PATCH] add routes to create user + route to delete user + add admin_required decorator --- .github/changelog.md | 7 ++- app/api/auth.py | 122 +++++++++++++++++++++++++++++++++++++++--- app/db/sqlite/auth.py | 22 ++++++-- manage.py | 6 +-- 4 files changed, 138 insertions(+), 19 deletions(-) diff --git a/.github/changelog.md b/.github/changelog.md index 1d00e7f1..4c1511df 100644 --- a/.github/changelog.md +++ b/.github/changelog.md @@ -1,7 +1,6 @@ # What's New? -- Hovering on recent favorite item will show how long ago it was ♥ed -- Recently added playlist returns a max of 100 tracks, but without a cutoff period + +- Auth -# Development -- API documentation on /openapi \ No newline at end of file +## Development diff --git a/app/api/auth.py b/app/api/auth.py index a22884fc..07a6da39 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -1,5 +1,6 @@ -from dataclasses import asdict import json +from dataclasses import asdict +from functools import wraps from flask import jsonify from flask_jwt_extended import create_access_token, current_user, set_access_cookies from pydantic import BaseModel, Field @@ -7,12 +8,29 @@ from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint from app.db.sqlite.auth import SQLiteAuthMethods as authdb -from app.utils.auth import check_password +from app.utils.auth import check_password, encode_password -bp_tag = Tag(name="Auth", description="Authentication") +bp_tag = Tag(name="Auth", description="Authentication stuff") api = APIBlueprint("auth", __name__, url_prefix="/auth", abp_tags=[bp_tag]) +def admin_required(): + """ + Decorator to require admin role + """ + + def wrapper(fn): + @wraps(fn) + def decorator(*args, **kwargs): + if "admin" not in current_user["roles"]: + return {"msg": "Only admins can do that!"}, 403 + return fn(*args, **kwargs) + + return decorator + + return wrapper + + class LoginBody(BaseModel): username: str = Field(description="The username", example="user0") password: str = Field(description="The password", example="password0") @@ -49,7 +67,6 @@ class UpdateProfileBody(BaseModel): @api.put("/profile/update") def update_profile(body: UpdateProfileBody): - user = { "id": current_user["id"], "email": body.email, @@ -68,11 +85,84 @@ def update_profile(body: UpdateProfileBody): else: user.pop("roles") + if user["password"]: + user["password"] = encode_password(user["password"]) + # remove empty values clean_user = {k: v for k, v in user.items() if v} return authdb.update_user(clean_user) +@api.post("/profile/create") +@admin_required() +def create_user(body: UpdateProfileBody): + if not body.username or not body.password: + return {"msg": "Username and password are required"}, 400 + + user = { + "username": body.username, + "password": encode_password(body.password), + "roles": json.dumps([]), + } + + # check if user already exists + if authdb.get_user_by_username(user["username"]): + return {"msg": "Username already exists"}, 400 + + userid = authdb.insert_user(user) + return authdb.get_user_by_id(userid).todict() + + +@api.post("/profile/guest/create") +@admin_required() +def create_guest_user(): + """ + Create a guest user + """ + # check if guest user already exists + guest_user = authdb.get_user_by_username("guest") + + if guest_user: + return { + "msg": "Guest user already exists", + }, 400 + + userid = authdb.insert_guest_user() + + if userid: + return { + "msg": "Guest user created", + } + + return { + "msg": "Failed to create guest user", + }, 500 + + +class DeleteUseBody(BaseModel): + username: str = Field("", description="The username") + + +@api.delete("/profile/delete") +@admin_required() +def delete_user(body: DeleteUseBody): + """ + Delete a user by username + """ + # prevent admin from deleting themselves + if body.username == current_user["usrname"]: + return {"msg": "Sorry! you cannot delete yourselfu"}, 400 + + # prevent deleting the only admin + users = authdb.get_all_users() + admins = [user for user in users if "admin" in user.roles] + if len(admins) == 1 and admins[0].username == body.username: + return {"msg": "Cannot delete the only admin"}, 400 + + authdb.delete_user_by_username(body.username) + return {"msg": f"User {body.username} deleted"} + + @api.get("/logout") def logout(): """ @@ -83,13 +173,32 @@ def logout(): return res +class GetAllUsersQuery(BaseModel): + simplified: bool = Field( + False, description="Whether to return simplified user data" + ) + + @api.get("/users") -def get_all_users(): +def get_all_users(query: GetAllUsersQuery): """ Get all users """ users = authdb.get_all_users() - return [user.todict_simplified() for user in users] + + # remove guest user + users = [user for user in users if user.username != "guest"] + + # reverse list to show latest users first + users = list(reversed(users)) + + # bring admins to the front + users = sorted(users, key=lambda x: "admin" in x.roles, reverse=True) + + if query.simplified: + return [user.todict_simplified() for user in users] + + return [user.todict() for user in users] @api.route("/user") @@ -97,5 +206,4 @@ def get_logged_in_user(): """ Get logged in user """ - print("current_user", current_user) return dict(current_user) diff --git a/app/db/sqlite/auth.py b/app/db/sqlite/auth.py index 83821500..2c36d183 100644 --- a/app/db/sqlite/auth.py +++ b/app/db/sqlite/auth.py @@ -28,11 +28,12 @@ class SQLiteAuthMethods: with SQLiteManager(userdata_db=True) as cur: cur = cur.execute(sql, user_tuple) userid = cur.lastrowid - - if userid: - user = SQLiteAuthMethods.get_user_by_id(userid).todict_simplified() - cur.close() - return user + return userid + # if userid: + # # sleep + # user = SQLiteAuthMethods.get_user_by_id(userid).todict_simplified() + # cur.close() + # return user raise Exception(f"Failed to insert user: {user}") @@ -131,3 +132,14 @@ class SQLiteAuthMethods: return User(*data) return None + + @staticmethod + def delete_user_by_username(username: str): + """ + Delete a user by username. + """ + sql = "DELETE FROM users WHERE username = ?" + + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql, (username,)) + cur.close() diff --git a/manage.py b/manage.py index de855e35..a3e106c9 100644 --- a/manage.py +++ b/manage.py @@ -60,9 +60,9 @@ def verify_auth(): # if request path starts with any of the blacklisted routes, don't verify jwt if any(request.path.startswith(route) for route in blacklist_routes): - print( - "Found blacklisted route: ", request.path, "... Skipping jwt verification" - ) + # print( + # "Found blacklisted route: ", request.path, "... Skipping jwt verification" + # ) return verify_jwt_in_request()