Skip to content

Commit

Permalink
Merge pull request #18 from ssciwr/fix_7_admin_edit_users
Browse files Browse the repository at this point in the history
Admins can edit user properties
  • Loading branch information
lkeegan authored Oct 2, 2024
2 parents bf008fa + 4f06c16 commit 19ff713
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 55 deletions.
18 changes: 4 additions & 14 deletions backend/src/predicTCR_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"])
Expand Down
25 changes: 19 additions & 6 deletions backend/src/predicTCR_server/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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]:
Expand Down
1 change: 1 addition & 0 deletions backend/tests/helpers/flask_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 36 additions & 0 deletions backend/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
120 changes: 85 additions & 35 deletions frontend/src/components/UsersTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Check warning on line 18 in frontend/src/components/UsersTable.vue

View workflow job for this annotation

GitHub Actions / Frontend :: node 22

'SignupComponent' is defined but never used
const props = defineProps<{
is_runner: boolean;
Expand All @@ -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
Expand All @@ -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();
})
Expand All @@ -78,9 +76,9 @@ function disable_user(user_email: string) {
<fwb-table-head-cell>Enabled</fwb-table-head-cell>
<fwb-table-head-cell>Full results</fwb-table-head-cell>
<fwb-table-head-cell>Quota</fwb-table-head-cell>
<fwb-table-head-cell>Last submission</fwb-table-head-cell>
<fwb-table-head-cell>Delay (mins)</fwb-table-head-cell>
<fwb-table-head-cell>Admin</fwb-table-head-cell>
<fwb-table-head-cell>Enable/disable</fwb-table-head-cell>
<fwb-table-head-cell>Actions</fwb-table-head-cell>
</fwb-table-head>
<fwb-table-body>
<fwb-table-row
Expand All @@ -94,27 +92,79 @@ function disable_user(user_email: string) {
<fwb-table-cell>{{ user.enabled ? "✓" : "✗" }}</fwb-table-cell>
<fwb-table-cell>{{ user.full_results ? "✓" : "✗" }}</fwb-table-cell>
<fwb-table-cell>{{ user.quota }}</fwb-table-cell>
<fwb-table-cell>
{{
new Date(user.last_submission_timestamp * 1000).toLocaleDateString(
"de-DE",
)
}}
</fwb-table-cell>
<fwb-table-cell>{{ user.submission_interval_minutes }}</fwb-table-cell>
<fwb-table-cell>{{ user.is_admin ? "✓" : "✗" }}</fwb-table-cell>
<fwb-table-cell>
<template v-if="user.enabled">
<fwb-button @click="disable_user(user.email)" color="red"
>Disable</fwb-button
>
</template>
<template v-else>
<fwb-button @click="enable_user(user.email)" color="green"
>Enable</fwb-button
>
</template>
<fwb-button
@click="
current_user = user;
show_modal = true;
"
class="mr-2"
>Edit</fwb-button
>
<fwb-button
@click="
current_user = user;
current_user.enabled = !current_user.enabled;
update_user();
"
:color="user.enabled ? 'red' : 'green'"
>{{ user.enabled ? "Disable" : "Enable" }}</fwb-button
>
</fwb-table-cell>
</fwb-table-row>
</fwb-table-body>
</fwb-table>

<fwb-modal size="lg" v-if="show_modal" @close="close_modal">
<template #header>
<div class="flex items-center text-lg">
Edit {{ current_user?.email }}
</div>
</template>
<template v-if="current_user" #body>
<div class="flex flex-col m-2 p-2">
<fwb-checkbox
v-model="current_user.activated"
label="Email address activated"
class="mb-2"
/>
<fwb-checkbox
v-model="current_user.enabled"
label="Account enabled"
class="mb-2"
/>
<fwb-checkbox
v-model="current_user.full_results"
label="Full results access"
class="mb-2"
/>
<fwb-range
v-model="current_user.quota"
:steps="1"
:min="0"
:max="99"
:label="`Remaining quota: ${current_user.quota}`"
class="mb-2"
/>
<fwb-range
v-model="current_user.submission_interval_minutes"
:steps="1"
:min="0"
:max="60"
:label="`Interval between submissions: ${current_user.submission_interval_minutes} minutes`"
class="mb-2"
/>
</div>
</template>
<template #footer>
<div class="flex justify-between">
<fwb-button @click="close_modal" color="alternative">
Cancel
</fwb-button>
<fwb-button @click="update_user" color="green"> Save </fwb-button>
</div>
</template>
</fwb-modal>
</template>
1 change: 1 addition & 0 deletions frontend/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export type User = {
is_admin: boolean;
is_runner: boolean;
full_results: boolean;
submission_interval_minutes: number;
};

0 comments on commit 19ff713

Please sign in to comment.