From 4f06c16464da883e0d23ead3653879f3dc6c4664 Mon Sep 17 00:00:00 2001 From: Liam Keegan Date: Wed, 2 Oct 2024 14:34:10 +0200 Subject: [PATCH] Admins can edit user properties - add submission interval parameter to User - add admin endpoint to update User - add UI to modify - activated - enabled - full_results - quota - submission interval - resolves #7 --- backend/src/predicTCR_server/app.py | 18 +--- backend/src/predicTCR_server/model.py | 25 +++-- backend/tests/helpers/flask_test_utils.py | 1 + backend/tests/test_app.py | 36 +++++++ frontend/src/components/UsersTable.vue | 120 +++++++++++++++------- frontend/src/utils/types.ts | 1 + 6 files changed, 146 insertions(+), 55 deletions(-) diff --git a/backend/src/predicTCR_server/app.py b/backend/src/predicTCR_server/app.py index a833586..34aa694 100644 --- a/backend/src/predicTCR_server/app.py +++ b/backend/src/predicTCR_server/app.py @@ -20,7 +20,7 @@ add_new_user, add_new_runner_user, reset_user_password, - enable_user, + update_user, activate_user, add_new_sample, get_samples, @@ -233,22 +233,12 @@ def admin_all_samples(): return jsonify(message="Admin account required"), 400 return jsonify(get_samples()) - @app.route("/api/admin/enable_user", methods=["POST"]) + @app.route("/api/admin/user", methods=["POST"]) @jwt_required() - def admin_enable_user(): + def admin_update_user(): if not current_user.is_admin: return jsonify(message="Admin account required"), 400 - user_email = request.json.get("user_email", "") - message, code = enable_user(user_email, True) - return jsonify(message=message), code - - @app.route("/api/admin/disable_user", methods=["POST"]) - @jwt_required() - def admin_disable_user(): - if not current_user.is_admin: - return jsonify(message="Admin account required"), 400 - user_email = request.json.get("user_email", "") - message, code = enable_user(user_email, False) + message, code = update_user(request.json) return jsonify(message=message), code @app.route("/api/admin/users", methods=["GET"]) diff --git a/backend/src/predicTCR_server/model.py b/backend/src/predicTCR_server/model.py index 4aa4525..17f34d3 100644 --- a/backend/src/predicTCR_server/model.py +++ b/backend/src/predicTCR_server/model.py @@ -68,6 +68,7 @@ class User(db.Model): activated: bool = db.Column(db.Boolean, nullable=False) enabled: bool = db.Column(db.Boolean, nullable=False) quota: int = db.Column(db.Integer, nullable=False) + submission_interval_minutes: int = db.Column(db.Integer, nullable=False) last_submission_timestamp: int = db.Column(db.Integer, nullable=False) is_admin: bool = db.Column(db.Boolean, nullable=False) is_runner: bool = db.Column(db.Boolean, nullable=False) @@ -100,6 +101,7 @@ def as_dict(self): "activated": self.activated, "enabled": self.enabled, "quota": self.quota, + "submission_interval_minutes": self.submission_interval_minutes, "last_submission_timestamp": self.last_submission_timestamp, "is_admin": self.is_admin, "is_runner": self.is_runner, @@ -237,6 +239,7 @@ def add_new_user(email: str, password: str, is_admin: bool) -> tuple[str, int]: activated=False, enabled=False, quota=predicTCR_submission_quota, + submission_interval_minutes=predicTCR_submission_interval_minutes, last_submission_timestamp=0, is_admin=is_admin, is_runner=False, @@ -272,6 +275,7 @@ def add_new_runner_user() -> User | None: activated=False, enabled=True, quota=0, + submission_interval_minutes=0, last_submission_timestamp=0, is_admin=False, is_runner=True, @@ -288,18 +292,27 @@ def add_new_runner_user() -> User | None: return None -def enable_user(email: str, enabled: bool) -> tuple[str, int]: - logger.info(f"Setting user {email} enabled to {enabled}") +def update_user(user_updates: dict) -> tuple[str, int]: + email = user_updates.get("email", "") + logger.info(f"Updating user {email}") user = db.session.execute( db.select(User).filter(User.email == email) ).scalar_one_or_none() if user is None: logger.info(f" -> Unknown email address '{email}'") - return f"Unknown email address {email}", 400 - user.activated = True - user.enabled = enabled + return f"Unknown email address {email}", 404 + for key in [ + "enabled", + "activated", + "quota", + "full_results", + "submission_interval_minutes", + ]: + value = user_updates.get(key, None) + if value is not None: + setattr(user, key, value) db.session.commit() - return f"Account {email} activated and enabled", 200 + return f"Account {email} updated", 200 def activate_user(token: str) -> tuple[str, int]: diff --git a/backend/tests/helpers/flask_test_utils.py b/backend/tests/helpers/flask_test_utils.py index 8a842b3..405cb5d 100644 --- a/backend/tests/helpers/flask_test_utils.py +++ b/backend/tests/helpers/flask_test_utils.py @@ -19,6 +19,7 @@ def add_test_users(app): activated=True, enabled=True, quota=1, + submission_interval_minutes=1, last_submission_timestamp=0, is_admin=is_admin, is_runner=is_runner, diff --git a/backend/tests/test_app.py b/backend/tests/test_app.py index bca8739..1f10a49 100644 --- a/backend/tests/test_app.py +++ b/backend/tests/test_app.py @@ -259,3 +259,39 @@ def test_runner_result_valid(client, result_zipfile): response = _upload_result(client, result_zipfile, 1) assert response.status_code == 200 assert "result processed" in response.json["message"].lower() + + +def test_admin_update_user_valid(client): + headers = _get_auth_headers(client, "admin@abc.xy", "admin") + user = client.get("/api/admin/users", headers=headers).json["users"][0] + invalid_update_keys = ["password", "idontexist"] + for invalid_update_key in invalid_update_keys: + user[invalid_update_key] = "this-will-be-ignored" + user["enabled"] = False + user["activated"] = False + user["quota"] = 99 + user["full_results"] = True + user["submission_interval_minutes"] = 17 + response = client.post("/api/admin/user", headers=headers, json=user) + assert response.status_code == 200 + assert user["email"] in response.json["message"] + assert "updated" in response.json["message"] + updated_user = client.get("/api/admin/users", headers=headers).json["users"][0] + for invalid_update_key in invalid_update_keys: + user.pop(invalid_update_key) + assert updated_user == user + + +def test_admin_update_user_invalid(client): + user_update = {"email": "Idontexist", "quota": 42} + # no auth header + response = client.post("/api/admin/user", json=user_update) + assert response.status_code == 401 + # valid non-admin user auth header + headers = _get_auth_headers(client) + response = client.post("/api/admin/user", headers=headers, json=user_update) + assert response.status_code == 400 + # invalid user email + headers = _get_auth_headers(client, "admin@abc.xy", "admin") + response = client.post("/api/admin/user", headers=headers, json=user_update) + assert response.status_code == 404 diff --git a/frontend/src/components/UsersTable.vue b/frontend/src/components/UsersTable.vue index 2571514..a9706bb 100644 --- a/frontend/src/components/UsersTable.vue +++ b/frontend/src/components/UsersTable.vue @@ -8,10 +8,14 @@ import { FwbTableHead, FwbTableHeadCell, FwbTableRow, + FwbModal, + FwbCheckbox, + FwbRange, } from "flowbite-vue"; import type { User } from "@/utils/types"; import { apiClient, logout } from "@/utils/api-client"; import { ref, computed } from "vue"; +import SignupComponent from "@/components/SignupComponent.vue"; const props = defineProps<{ is_runner: boolean; @@ -23,6 +27,13 @@ const filtered_users = computed(() => { return user.is_runner === props.is_runner; }); }); +const current_user = ref(null as User | null); + +const show_modal = ref(false); +function close_modal() { + show_modal.value = false; + get_users(); +} function get_users() { apiClient @@ -40,23 +51,10 @@ function get_users() { get_users(); -function enable_user(user_email: string) { +function update_user() { + show_modal.value = false; apiClient - .post("admin/enable_user", { user_email: user_email }) - .then(() => { - get_users(); - }) - .catch((error) => { - if (error.response.status > 400) { - logout(); - } - console.log(error); - }); -} - -function disable_user(user_email: string) { - apiClient - .post("admin/disable_user", { user_email: user_email }) + .post("admin/user", current_user.value) .then(() => { get_users(); }) @@ -78,9 +76,9 @@ function disable_user(user_email: string) { Enabled Full results Quota - Last submission + Delay (mins) Admin - Enable/disable + Actions {{ user.enabled ? "✓" : "✗" }} {{ user.full_results ? "✓" : "✗" }} {{ user.quota }} - - {{ - new Date(user.last_submission_timestamp * 1000).toLocaleDateString( - "de-DE", - ) - }} - + {{ user.submission_interval_minutes }} {{ user.is_admin ? "✓" : "✗" }} - - + Edit + {{ user.enabled ? "Disable" : "Enable" }} + + + + + + diff --git a/frontend/src/utils/types.ts b/frontend/src/utils/types.ts index cf547ca..ccc797a 100644 --- a/frontend/src/utils/types.ts +++ b/frontend/src/utils/types.ts @@ -19,4 +19,5 @@ export type User = { is_admin: boolean; is_runner: boolean; full_results: boolean; + submission_interval_minutes: number; };