diff --git a/compendium_v2/routes/nren.py b/compendium_v2/routes/nren.py index df9e134bd35b78d7e7db34e7f1186e3eda935280..dc1c075c53913f2782b51b910edd14c73244f558 100644 --- a/compendium_v2/routes/nren.py +++ b/compendium_v2/routes/nren.py @@ -1,7 +1,7 @@ import logging from typing import Any -from flask import Blueprint, jsonify, request +from flask import Blueprint, jsonify from compendium_v2.routes import common from compendium_v2.db.auth_model import NREN diff --git a/compendium_v2/routes/user.py b/compendium_v2/routes/user.py index defc343238f1d66b5c2a0628a8f4aff08d625d41..d3326ec2ef2181c9d7cd0576f8be2265aeb73171 100644 --- a/compendium_v2/routes/user.py +++ b/compendium_v2/routes/user.py @@ -3,14 +3,13 @@ from typing import Any, Union from flask import Blueprint, jsonify, request from flask_login import current_user, AnonymousUserMixin, login_required # type: ignore -from sqlalchemy import select, insert +from sqlalchemy import select from compendium_v2.auth.session_management import admin_required from compendium_v2.db import db -from compendium_v2.db.auth_model import User, ROLES, UserNrenMember +from compendium_v2.db.auth_model import User, ROLES from compendium_v2.db.model import NREN from compendium_v2.routes import common -from compendium_v2.survey_db.model import Nrens routes = Blueprint('user', __name__) logger = logging.getLogger(__name__) @@ -31,11 +30,18 @@ USER_RESPONSE_SCHEMA = { 'user': { 'type': 'object', 'properties': { - 'name': {'type': 'string'}, + 'name': {'type': ['string', 'null']}, 'email': {'type': ['string', 'null']}, 'permissions': {'$ref': '#/definitions/permissions'}, + 'id': {'type': 'uuid'}, + 'role': {'type': 'string'}, + 'oidc_sub': {'type': 'string'}, + 'nrens': { + 'type': 'array', + 'items': {'type': 'string'} + } }, - 'required': ['name', 'email', 'permissions'], + 'required': ['permissions'], 'additionalProperties': False } }, @@ -45,6 +51,28 @@ USER_RESPONSE_SCHEMA = { } +def _extract_user(user: Union[User, AnonymousUserMixin]): + if isinstance(user, AnonymousUserMixin): + return { + 'permissions': { + 'admin': False, + 'active': False, + } + } + return { + 'name': user.fullname, + 'email': user.email, + 'permissions': { + 'admin': user.roles == ROLES.admin, + 'active': user.active, + }, + 'id': user.id, + 'role': user.roles.value, + 'oidc_sub': user.oidc_sub, + 'nrens': [nren.name for nren in user.nrens] + } + + @routes.route('/', methods=['GET']) @common.require_accepts_json def current_user_view() -> Any: @@ -59,61 +87,25 @@ def current_user_view() -> Any: :return: """ - def _extract_data(entry: Union[User, AnonymousUserMixin]): - if isinstance(entry, AnonymousUserMixin): - return { - 'name': '', - 'email': None, - 'permissions': { - 'admin': False, - 'active': False, - } - } - return { - 'name': entry.fullname, - 'email': entry.email, - 'permissions': { - 'admin': entry.roles == ROLES.admin, - 'active': entry.active, - }, - 'id': entry.id, - 'roles': entry.roles.value, - 'active': entry.active, - 'full_name': entry.fullname, - 'oidc_sub': entry.oidc_sub, - 'nrens': [nren.name for nren in entry.nrens] - } - - return jsonify(_extract_data(current_user)) + return jsonify(_extract_user(current_user)) @routes.route('/list', methods=['GET']) @common.require_accepts_json -# @login_required -# @admin_required +@login_required +@admin_required def all_users_view() -> Any: # TODO schema and docstring - def _extract_data(entry: User): - return { - 'id': entry.id, - 'email': entry.email, - 'roles': entry.roles.value, - 'active': entry.active, - 'full_name': entry.fullname, - 'oidc_sub': entry.oidc_sub, - 'nrens': [nren.name for nren in entry.nrens] - } - - entries = [_extract_data(entry) for entry in db.session.scalars( + entries = [_extract_user(user) for user in db.session.scalars( select(User).order_by(User.email)).unique()] return jsonify(entries) @routes.route('/', methods=['PUT']) @common.require_accepts_json -# @login_required -# @admin_required +@login_required +@admin_required def update_user_view() -> Any: """ Handler for updating user information via PUT request. @@ -132,82 +124,47 @@ def update_user_view() -> Any: :return: """ - def _get_user_by_id(user_id): - return db.session.execute(select(User).filter_by(id=user_id)).scalar() - # user_data = db.session.execute(select(User).filter_by(id=user_id)).scalar() - # return _extract_data(user_data) if user_data else None - - def _extract_data(entry: Union[User, AnonymousUserMixin]): - if isinstance(entry, AnonymousUserMixin): - return { - 'name': 'Anonymous User', - } - return { - 'name': entry.fullname, - 'id': entry.id, - 'email': entry.email, - 'roles': entry.roles.value, - 'active': entry.active, - 'full_name': entry.fullname, - 'oidc_sub': entry.oidc_sub, - # 'nrens': entry.nrens - 'nrens': [nren.id for nren in entry.nrens] - - } - - def _update_user_data(user: Union[User, AnonymousUserMixin], update_data: dict): - if isinstance(user, AnonymousUserMixin): - raise ValueError("Cannot update Anonymous User") - - # Example: Update user fields if they exist in the request data - if isinstance(user, User): - if 'name' in update_data: - user.fullname = update_data['name'] - if 'email' in update_data: - user.email = update_data['email'] - if 'roles' in update_data: - if isinstance(user.roles, ROLES): - user.roles = ROLES[update_data['roles']] - else: - user.roles = update_data['roles'] - if 'active' in update_data: - user.active = update_data['active'] or update_data['active'] == 'on' \ - or update_data['active'].lower() == 'true' - else: - raise ValueError("Invalid user object provided") + def _update_user_data(user: User, update_data: dict): - if 'nrens' in update_data: - new_nren_ids = db.session.scalars(select(NREN).filter(NREN.id.in_(update_data['nrens']))).all() - user.nrens = new_nren_ids + new_roles = update_data.get('roles', user.roles.value) - # Delete existing entries in UserNrenMember table for the user - # UserNrenMember.query.filter_by(user_id=user.id).delete() + if new_roles != user.roles.value: + if user == current_user: + return jsonify({'success': False, 'message': 'Cannot change your own role.'}), 400 - # Insert new entries into UserNrenMember table with updated nren_id values - # if new_nren_ids: - # for nren_id in new_nren_ids: - # db.session.execute(insert(UserNrenMember).values(user_id=user.id, nren_id=nren_id)) + user.roles = new_roles - db.session.commit() + if 'active' in update_data: + _active = bool(update_data['active']) + if _active != user.active: + if user == current_user: + return jsonify({'success': False, 'message': 'Cannot deactivate yourself.'}), 400 - try: - update_data = request.get_json() - if not update_data: - return jsonify({"error": "No data provided in the request."}), 400 + user.active = _active - user_id_to_pull = update_data.get("id") - if not user_id_to_pull: - return jsonify({"error": "No user ID provided in the request data."}), 400 + nrens = update_data.get('nrens', None) + if nrens is not None: + new_nrens = None + try: + new_nrens = db.session.scalars(select(NREN).filter(NREN.id.in_(update_data['nrens']))).all() + user.nrens = [nren for nren in new_nrens] + db.session.commit() + except Exception: + return jsonify({'success': False, 'message': 'No valid NREN IDs provided.'}), 400 - current_user_data = _get_user_by_id(user_id_to_pull) + return jsonify({'success': True, 'message': 'User updated successfully'}) - current_user_json = _extract_data(current_user_data) + body = request.get_json() + if not body: + return jsonify({"success": False, 'message': 'Invalid request'}), 400 - _update_user_data(current_user_data, update_data) + user_id = body.get("id") + if not user_id: + return jsonify({"success": False, 'message': 'No user ID provided in the request data.'}), 400 - updated_user_json = _extract_data(current_user_data) + user = db.session.execute(select(User).filter_by(id=user_id)).scalar() - return jsonify({"old_data": current_user_json, "updated_data": updated_user_json}) + if not user: + return jsonify({"success": False, 'message': 'User not found.'}), 404 - except ValueError as e: - return jsonify({"error": str(e)}), 400 + return _update_user_data(user, body) diff --git a/survey-frontend/src/App.tsx b/survey-frontend/src/App.tsx index df23656751291019c9f6a6894065037af560d1c6..f26d94d106bc2a37dc408a15c7fbf3515804bf72 100644 --- a/survey-frontend/src/App.tsx +++ b/survey-frontend/src/App.tsx @@ -6,7 +6,7 @@ import SurveyManagementComponent from './SurveyManagementComponent'; import UserManagementComponent from './UserManagementComponent'; import SurveyContainerComponent from "./SurveyContainerComponent"; import ExternalPageNavBar from "shared/ExternalPageNavBar" -import UserProvider from "shared/UserProvider"; +import UserProvider from "./providers/UserProvider"; function App(): ReactElement { diff --git a/survey-frontend/src/Schema.tsx b/survey-frontend/src/Schema.tsx index 071dc924340ea3ae061d47e14ac6b3d5d9f7184b..b3bc786d5df529e45b4d5345289c3aa6c85ba164 100644 --- a/survey-frontend/src/Schema.tsx +++ b/survey-frontend/src/Schema.tsx @@ -1,3 +1,22 @@ +export interface User { + id: string, + email: string, + role: string, + name: string, + oidc_sub: string, + nrens: string[], + permissions: { + admin: boolean, + active: boolean, + }, + editable: boolean, +} + +export interface Nren { + id: string, + name: string, + country: string +} export enum VerificationStatus { New = "new", // a question that was not answered last year diff --git a/survey-frontend/src/UserManagementComponent.tsx b/survey-frontend/src/UserManagementComponent.tsx index e99a2091cf010b9c1ecaefe71ed7a0e51308ab25..78d8b5dd9ca1bbc0d22c070bc510977e8dbe903f 100644 --- a/survey-frontend/src/UserManagementComponent.tsx +++ b/survey-frontend/src/UserManagementComponent.tsx @@ -1,5 +1,7 @@ -import React, { useState, useEffect } from "react"; -import {Button, Table} from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; +import { Button, Table } from "react-bootstrap"; +import { userContext } from "./providers/UserProvider"; +import { User, Nren } from "./Schema"; async function fetchUsers(): Promise<User[]> { @@ -26,67 +28,31 @@ async function fetchNrens(): Promise<Nren[]> { const saveUser = (user) => { const requestOptions = { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - // You might need to include authorization headers if required by your API. - // 'Authorization': 'Bearer your_access_token', - }, - body: JSON.stringify(user), + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(user), }; - - fetch('/api/user', requestOptions) - .then((response) => { - if (!response.ok) { - throw new Error('Network response was not ok.'); - } - - // return response.json(); - }) - .catch((error) => { - console.error('Error:', error); - }); - }; - - -async function fetchLoggedInUser() : Promise<User>{ - try { - const response = await fetch('/api/user'); - if (response.ok) { - const user = await response.json(); - console.log(user) - return user; - - } else { - throw new Error('Failed to fetch logged-in user'); - } - } catch (error) { - console.error(error); - throw error; - } -} -interface User { - id: string, - email: string, - roles: string, - active: boolean, - full_name: string, - oidc_sub:string, - nrens: string[], - editable: boolean, -} + fetch('/api/user', requestOptions) + .then((response) => { + return response.json(); + }) + .catch((error) => { + const response = error.response; + response.json().then((json) => { + console.log(json); + }); + }); +}; -interface Nren{ - id:string, - name:string, - country:string -} function UserManagementComponent() { const [users, setUsers] = useState<User[]>([]); const [nrens, setNrens] = useState<Nren[]>([]); - const [loggedInUser, setLoggedInUser] = useState<User | null>(null); + const { user: loggedInUser } = useContext(userContext); + console.log(loggedInUser) useEffect(() => { // Fetch user @@ -94,14 +60,9 @@ function UserManagementComponent() { setUsers(userList); }); - fetchNrens().then((nrenList)=>{ - setNrens(nrenList) + fetchNrens().then((nrenList) => { + setNrens(nrenList.sort((a, b) => a.name.localeCompare(b.name))) }) - - // Fetch logged-in user - fetchLoggedInUser().then((user) => { - setLoggedInUser(user); - }); }, []); const handleEdit = (user: User) => { @@ -125,15 +86,9 @@ function UserManagementComponent() { const index = users.findIndex((u) => u.id === user.id); const updatedUsers = [...users]; - // const { name, type, value } = event.target; - // if (type === 'checkbox') { - // user[name] = (event.target as HTMLInputElement).checked;; - // } else { - // user[name] = (event.target as HTMLInputElement).value; - // } const { name, type } = event.target; if (type === 'checkbox') { - user[name] = (event.target as HTMLInputElement).checked ? 'true' : 'false'; // Set "true" or "false" as strings + user[name] = (event.target as HTMLInputElement).checked ? true : false; } else { user[name] = (event.target as HTMLInputElement).value; } @@ -145,89 +100,100 @@ function UserManagementComponent() { setUsers(updatedUsers); }; + const findNren = (value) => { + return nrens.find((nren) => { + return nren.id == value || nren.name == value + })?.name + } + return ( <div> <h1> User Management Page</h1> <Table> <thead> <tr> - <th className='pt-3' style={{border: "1px solid #ddd"}}> Id </th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> Active </th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> Roles </th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> Email</th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> Full Name</th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> OIDC Sub</th> - <th className='pt-3' style={{border: "1px solid #ddd"}}> NREN</th> - <th className="pt-3" style={{border: "1px solid #ddd"}}>Actions</th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> Id </th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> Active </th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> Roles </th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> Email</th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> Full Name</th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> OIDC Sub</th> + <th className='pt-3' style={{ border: "1px solid #ddd" }}> NREN</th> + <th className="pt-3" style={{ border: "1px solid #ddd" }}>Actions</th> </tr> </thead> <tbody> - {users.map(user => ( - <tr key={user.id}> - <td style={{border: "1px dotted #ddd"}}>{user.id}</td> - <td style={{ border: "1px dotted #ddd" }}> - {user.editable ? ( - <input - type="checkbox" - name="active" - checked={user.active} - onChange={(event) => handleInputChange(event, user)} - /> - ) : ((user.active ? 'Active' : 'Inactive'))} - </td> - <td style={{ border: "1px dotted #ddd" }}> - {user.editable ? ( - <select - name="roles" - value={user.roles} - onChange={(event) => handleInputChange(event, user)}> - <option value="admin">Admin</option> - <option value="user">User</option> - </select> - ) : ( - user.roles - )} - </td> - - <td style={{border: "1px dotted #ddd"}}> - {(user.email)} - </td> - <td style={{border: "1px dotted #ddd"}}> - {(user.full_name)} - </td> - <td style={{border: "1px dotted #ddd"}}> - {(user.oidc_sub)} - </td> - <td style={{ border: '1px dotted #ddd' }}> - {user.editable ? ( - nrens.length > 0 ? ( + {users.map(user => ( + <tr key={user.id}> + <td style={{ border: "1px dotted #ddd" }}>{user.id}</td> + <td style={{ border: "1px dotted #ddd" }}> + {user.editable ? ( + <input + type="checkbox" + name="active" + checked={user.permissions.active} + onChange={(event) => handleInputChange(event, user)} + /> + ) : ((user.permissions.active ? 'Active' : 'Inactive'))} + </td> + <td style={{ border: "1px dotted #ddd" }}> + {user.editable ? ( <select - name="nrens" - value={user.nrens} - multiple={false} + name="roles" + value={user.role} onChange={(event) => handleInputChange(event, user)}> - {nrens.map((nren) => ( - <option key={nren.id} value={nren.id}> - {nren.name} - </option> - ))} + <option value="admin">Admin</option> + <option value="user">User</option> </select> ) : ( - <div>No options available</div> - ) - ) : ( - user.nrens.length > 0 ? user.nrens : "NREN not selected" - )} - </td> - <td style={{border: "1px dotted #ddd"}}> - {loggedInUser?.roles.includes("admin") && user.editable ? ( - <Button onClick={() => handleSave(user)}>Save</Button> - ) : loggedInUser?.roles.includes('admin') && !user.editable ? ( - <Button onClick={() => handleEdit(user)}>Edit</Button> - ) : null} - </td> - </tr> - ))} + user.role + )} + </td> + + <td style={{ border: "1px dotted #ddd" }}> + {(user.email)} + </td> + <td style={{ border: "1px dotted #ddd" }}> + {(user.name)} + </td> + <td style={{ border: "1px dotted #ddd" }}> + {(user.oidc_sub)} + </td> + <td style={{ border: '1px dotted #ddd' }}> + {user.editable ? ( + nrens.length > 0 ? ( + <select + name="nrens" + multiple={false} + defaultValue={user.nrens.length > 0 ? nrens.find((nren) => { + return nren.name == user.nrens[0] || nren.id == user.nrens[0] + })?.id : undefined} + onChange={(event) => handleInputChange(event, user)}> + <option> + Select NREN + </option> + {nrens.map((nren) => ( + <option key={nren.id} value={nren.id}> + {nren.name} + </option> + ))} + </select> + ) : ( + <div>No options available</div> + ) + ) : ( + (user.nrens?.length || 0) > 0 ? findNren(user.nrens[0]) : "NREN not selected" + )} + </td> + <td style={{ border: "1px dotted #ddd" }}> + {loggedInUser?.role == 'admin' && user.editable ? ( + <Button onClick={() => handleSave(user)}>Save</Button> + ) : loggedInUser?.role == 'admin' && !user.editable ? ( + <Button onClick={() => handleEdit(user)}>Edit</Button> + ) : null} + </td> + </tr> + ))} </tbody> </Table> diff --git a/survey-frontend/src/providers/UserProvider.tsx b/survey-frontend/src/providers/UserProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..44bc874497441c86d5cf2006f73bfab76f3ae62c --- /dev/null +++ b/survey-frontend/src/providers/UserProvider.tsx @@ -0,0 +1,48 @@ +import React, { createContext, useState, useEffect } from 'react'; +import { User } from '../Schema'; + +interface Props { + children: React.ReactNode; +} + + +async function fetchUser(): Promise<User> { + const response = await fetch('/api/user'); + const user = await response.json(); + return user +} + +const anonymousUser: User = { 'name': '', email: '', permissions: { admin: false, active: false }, editable: false, id: '', nrens: [], oidc_sub: '', role: '' }; + +const userContext = createContext<{ + user: User; + logout: () => void; +}>({ + user: anonymousUser, + logout: () => { } +}); + + +const UserProvider: React.FC<Props> = ({ children }) => { + const [user, setUser] = useState<User>(anonymousUser); + + async function logoutUser() { + await fetch('/logout'); + setUser(anonymousUser); + } + + useEffect(() => { + fetchUser().then(user => { + setUser(user) + }); + }, []); + + return ( + <userContext.Provider value={{ user, logout: logoutUser }}> + {children} + </userContext.Provider> + ); +}; + +export { userContext }; +export default UserProvider; \ No newline at end of file