From 1da959d1b41d1e6a32d7e30e8ca052ea8bb3eaee Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Mon, 25 Nov 2024 13:41:56 +0000 Subject: [PATCH] Placeholder --- pydatalab/pydatalab/routes/v0_1/users.py | 42 ++++++ pydatalab/tests/server/test_users.py | 13 ++ .../components/EditAccountSettingsModal.vue | 126 ++++++++++++------ webapp/src/server_fetch_utils.js | 28 ++-- 4 files changed, 156 insertions(+), 53 deletions(-) diff --git a/pydatalab/pydatalab/routes/v0_1/users.py b/pydatalab/pydatalab/routes/v0_1/users.py index 02c87732b..285e38ff9 100644 --- a/pydatalab/pydatalab/routes/v0_1/users.py +++ b/pydatalab/pydatalab/routes/v0_1/users.py @@ -3,12 +3,54 @@ from flask_login import current_user from pydatalab.config import CONFIG +from pydatalab.logger import logged_route from pydatalab.models.people import DisplayName, EmailStr from pydatalab.mongo import flask_mongo USERS = Blueprint("users", __name__) +@USERS.route("/users//remove-identity", methods=["DELETE"]) +@logged_route +def remove_user_identity(user_id): + """Given a user ID, delete a specified identity from the user, + passed in the request body as a JSON object of the form `{"identity_type": }`. + """ + request_json = request.get_json() + identity_to_remove: dict[str, str] | None = None + if request_json is not None: + identity_to_remove = request_json.get("identity", None) + + if not current_user.is_authenticated and not CONFIG.TESTING: + return (jsonify({"status": "error", "message": "No user authenticated."}), 401) + + if not CONFIG.TESTING and current_user.id != user_id and current_user.role != "admin": + return ( + jsonify({"status": "error", "message": "User not allowed to edit this profile."}), + 403, + ) + + if not identity_to_remove: + return jsonify({"status": "error", "message": "No identity provided."}), 400 + + if len(identity_to_remove) != 1: + return jsonify( + {"status": "error", "message": "Only one identity can be removed at a time."} + ), 400 + + identity_type, identity_value = list(identity_to_remove.items())[0] + + pull = {"identities": {"identity_type": identity_type, "identifier": str(identity_value)}} + update_result = flask_mongo.db.users.update_one({"_id": ObjectId(user_id)}, {"$pull": pull}) + if update_result.matched_count != 1: + return jsonify({"status": "error", "message": "Unable to update user."}), 400 + + if update_result.modified_count != 1: + return jsonify({"status": "success", "message": "No update was performed"}), 200 + + return jsonify({"status": "success"}), 200 + + @USERS.route("/users/", methods=["PATCH"]) def save_user(user_id): request_json = request.get_json() diff --git a/pydatalab/tests/server/test_users.py b/pydatalab/tests/server/test_users.py index 64eab34e8..468a93a30 100644 --- a/pydatalab/tests/server/test_users.py +++ b/pydatalab/tests/server/test_users.py @@ -114,3 +114,16 @@ def test_user_update_admin(admin_client, real_mongo_client, user_id): assert resp.status_code == 200 user = real_mongo_client.get_database().users.find_one({"_id": user_id}) assert user["display_name"] == "Test Person" + + +def test_user_remove_identity(client, real_mongo_client, user_id): + endpoint = f"/users/{str(user_id)}/remove-identity" + + real_mongo_client.get_database().users.update_one( + {"_id": user_id}, + {"$set": {"identities": [{"identity_type": "orcid", "identifier": "0000-0000-0000-0000"}]}}, + ) + user_request = client.delete(endpoint, json={"orcid": "0000-0000-0000-0000"}) + assert user_request.status_code == 200 + user = real_mongo_client.get_database().users.find_one({"_id": user_id}) + assert not user.get("identities", None) diff --git a/webapp/src/components/EditAccountSettingsModal.vue b/webapp/src/components/EditAccountSettingsModal.vue index 4f63219be..c06f41cab 100644 --- a/webapp/src/components/EditAccountSettingsModal.vue +++ b/webapp/src/components/EditAccountSettingsModal.vue @@ -48,52 +48,67 @@
@@ -128,7 +143,12 @@ import { API_URL } from "@/resources.js"; import Modal from "@/components/Modal.vue"; import UserBubble from "@/components/UserBubble.vue"; -import { getUserInfo, saveUser, requestNewAPIKey } from "@/server_fetch_utils.js"; +import { + getUserInfo, + saveUser, + requestNewAPIKey, + disconnectIdentityFromUser, +} from "@/server_fetch_utils.js"; import StyledInput from "./StyledInput.vue"; export default { @@ -189,6 +209,9 @@ export default { this.$store.commit("setDisplayName", this.user.display_name); this.$emit("update:modelValue", false); }, + async disconnectIdentity(identityType, identifier) { + await disconnectIdentityFromUser(this.user.immutable_id, identityType, identifier); + }, async getUser() { let user = await getUserInfo(); if (user != null) { @@ -234,6 +257,23 @@ export default { color: red; } +.identity-section:hover { + background-color: inherit; +} + +.identity-link { + color: black; +} + +.identity-disconnect-btn { + padding-left: 10px; + color: darkgrey; +} + +.identity-disconnect-btn:hover { + color: black; +} + :deep(.form-error a) { color: #820000; font-weight: 600; diff --git a/webapp/src/server_fetch_utils.js b/webapp/src/server_fetch_utils.js index 876392f00..c8aba4841 100644 --- a/webapp/src/server_fetch_utils.js +++ b/webapp/src/server_fetch_utils.js @@ -69,11 +69,12 @@ function fetch_put(url, body) { } // eslint-disable-next-line no-unused-vars -function fetch_delete(url) { +function fetch_delete(url, body) { let headers = construct_headers({ "Content-Type": "application/json" }); const requestOptions = { method: "DELETE", headers: headers, + body: JSON.stringify(body), credentials: "include", }; return fetch(url, requestOptions).then(handleResponse); @@ -404,15 +405,6 @@ export function deleteEquipment(item_id) { .catch((error) => alert("Item delete failed for " + item_id + ": " + error)); } -export function deletSampleFromCollection(collection_id, collection_summary) { - return fetch_delete(`${API_URL}/collections/${collection_id}`) - .then(function (response_json) { - console.log("delete successful" + response_json); - store.commit("deleteFromCollectionList", collection_summary); - }) - .catch((error) => alert("Collection delete failed for " + collection_id + ": " + error)); -} - export async function getItemData(item_id) { return fetch_get(`${API_URL}/get-item-data/${item_id}`) .then((response_json) => { @@ -569,6 +561,22 @@ export function saveUser(user_id, user) { }); } +export function disconnectIdentityFromUser(userId, identityType, identifier) { + fetch_delete(`${API_URL}/users/${userId}/remove-identity`, { + identity: { identityType: identifier }, + }) + .then(function (response_json) { + if (response_json.status === "success") { + getUserInfo(); + } else { + alert("Identity disconnect unsuccessful", response_json.detail); + } + }) + .catch(function (error) { + alert(`Identity disconnect unsuccessful: ${error}`); + }); +} + export function saveRole(user_id, role) { fetch_patch(`${API_URL}/roles/${user_id}`, role) .then(function (response_json) {