Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admins can edit user properties #18

Merged
merged 1 commit into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@

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 @@
<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 @@
<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;
};
Loading