Skip to content
Snippets Groups Projects
Commit b80a5bf7 authored by Bjarke Madsen's avatar Bjarke Madsen
Browse files

Merge branch 'COMP-240' into 'develop'

COMP-240

See merge request !55
parents 8190932e 3edc352f
No related branches found
No related tags found
1 merge request!55COMP-240
...@@ -12,6 +12,7 @@ from compendium_v2.routes.ec_projects import routes as ec_routes ...@@ -12,6 +12,7 @@ from compendium_v2.routes.ec_projects import routes as ec_routes
from compendium_v2.routes.policy import routes as policy from compendium_v2.routes.policy import routes as policy
from compendium_v2.routes.survey import routes as survey from compendium_v2.routes.survey import routes as survey
from compendium_v2.routes.user import routes as user_routes from compendium_v2.routes.user import routes as user_routes
from compendium_v2.routes.nren import routes as nren_routes
routes = Blueprint('compendium-v2-api', __name__) routes = Blueprint('compendium-v2-api', __name__)
routes.register_blueprint(budget_routes, url_prefix='/budget') routes.register_blueprint(budget_routes, url_prefix='/budget')
...@@ -23,7 +24,7 @@ routes.register_blueprint(ec_routes, url_prefix='/ec-project') ...@@ -23,7 +24,7 @@ routes.register_blueprint(ec_routes, url_prefix='/ec-project')
routes.register_blueprint(policy, url_prefix='/policy') routes.register_blueprint(policy, url_prefix='/policy')
routes.register_blueprint(survey, url_prefix='/survey') routes.register_blueprint(survey, url_prefix='/survey')
routes.register_blueprint(user_routes, url_prefix='/user') routes.register_blueprint(user_routes, url_prefix='/user')
routes.register_blueprint(nren_routes, url_prefix='/nren')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
import logging
from typing import Any
from flask import Blueprint, jsonify
from compendium_v2.routes import common
from compendium_v2.db.auth_model import NREN
from sqlalchemy import select
from compendium_v2.db import db
routes = Blueprint('nren', __name__)
logger = logging.getLogger(__name__)
NREN_RESPONSE_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'nren': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'country': {'type': 'string'},
},
'additionalProperties': False
}
},
'type': 'array',
'items': {'$ref': '#/definitions/nren'}
}
@routes.route('/list', methods=['GET'])
@common.require_accepts_json
def all_nrens_view() -> Any:
def _extract_nren_data(entry: NREN):
return {
'id': entry.id,
'name': entry.name,
'country': entry.country
}
nren_entries = [_extract_nren_data(nren) for nren in db.session.scalars(select(NREN))]
return jsonify(nren_entries)
import logging import logging
from typing import Any, Union from typing import Any, Union
from flask import Blueprint, jsonify from flask import Blueprint, jsonify, request
from flask_login import current_user, AnonymousUserMixin # type: ignore from flask_login import current_user, AnonymousUserMixin, login_required # type: ignore
from sqlalchemy import select from sqlalchemy import select
from compendium_v2.auth.session_management import admin_required
from compendium_v2.db import db from compendium_v2.db import db
from compendium_v2.db.auth_model import User, ROLES 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.routes import common
routes = Blueprint('user', __name__) routes = Blueprint('user', __name__)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -29,11 +30,18 @@ USER_RESPONSE_SCHEMA = { ...@@ -29,11 +30,18 @@ USER_RESPONSE_SCHEMA = {
'user': { 'user': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': {'type': 'string'}, 'name': {'type': ['string', 'null']},
'email': {'type': ['string', 'null']}, 'email': {'type': ['string', 'null']},
'permissions': {'$ref': '#/definitions/permissions'}, '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 'additionalProperties': False
} }
}, },
...@@ -43,6 +51,28 @@ USER_RESPONSE_SCHEMA = { ...@@ -43,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']) @routes.route('/', methods=['GET'])
@common.require_accepts_json @common.require_accepts_json
def current_user_view() -> Any: def current_user_view() -> Any:
...@@ -57,42 +87,84 @@ def current_user_view() -> Any: ...@@ -57,42 +87,84 @@ def current_user_view() -> Any:
:return: :return:
""" """
def _extract_data(entry: Union[User, AnonymousUserMixin]): return jsonify(_extract_user(current_user))
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,
}
}
return jsonify(_extract_data(current_user))
@routes.route('/list', methods=['GET']) @routes.route('/list', methods=['GET'])
@common.require_accepts_json @common.require_accepts_json
@login_required
@admin_required
def all_users_view() -> Any: def all_users_view() -> Any:
# TODO schema and docstring # TODO schema and docstring
def _extract_data(entry: User): entries = [_extract_user(user) for user in db.session.scalars(
return {
'id': entry.id,
'email': entry.email,
'roles': entry.roles.value,
'active': entry.active,
'nrens': [nren.name for nren in entry.nrens]
}
entries = [_extract_data(entry) for entry in db.session.scalars(
select(User).order_by(User.email)).unique()] select(User).order_by(User.email)).unique()]
return jsonify(entries) return jsonify(entries)
@routes.route('/', methods=['PUT'])
@common.require_accepts_json
@login_required
@admin_required
def update_user_view() -> Any:
"""
Handler for updating user information via PUT request.
Request data should be in JSON format with the fields you want to update.
The response will be formatted the same way as in the current_user_view.
Example JSON request data:
{
"name": "Updated Name",
"email": "updated@example.com",
"roles": ["role1", "role2"],
"active": False
}
:return:
"""
def _update_user_data(user: User, update_data: dict):
new_roles = update_data.get('roles', user.roles.value)
if new_roles != user.roles.value:
if user == current_user:
return jsonify({'success': False, 'message': 'Cannot change your own role.'}), 400
user.roles = new_roles
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
user.active = _active
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
return jsonify({'success': True, 'message': 'User updated successfully'})
body = request.get_json()
if not body:
return jsonify({"success": False, 'message': 'Invalid request'}), 400
user_id = body.get("id")
if not user_id:
return jsonify({"success": False, 'message': 'No user ID provided in the request data.'}), 400
user = db.session.execute(select(User).filter_by(id=user_id)).scalar()
if not user:
return jsonify({"success": False, 'message': 'User not found.'}), 404
return _update_user_data(user, body)
...@@ -6,7 +6,7 @@ import SurveyManagementComponent from './SurveyManagementComponent'; ...@@ -6,7 +6,7 @@ import SurveyManagementComponent from './SurveyManagementComponent';
import UserManagementComponent from './UserManagementComponent'; import UserManagementComponent from './UserManagementComponent';
import SurveyContainerComponent from "./SurveyContainerComponent"; import SurveyContainerComponent from "./SurveyContainerComponent";
import ExternalPageNavBar from "shared/ExternalPageNavBar" import ExternalPageNavBar from "shared/ExternalPageNavBar"
import UserProvider from "shared/UserProvider"; import UserProvider from "./providers/UserProvider";
function App(): ReactElement { function App(): ReactElement {
......
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 { export enum VerificationStatus {
New = "new", // a question that was not answered last year New = "new", // a question that was not answered last year
......
import React, { useState, useEffect } from "react"; 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[]> { async function fetchUsers(): Promise<User[]> {
...@@ -12,33 +15,188 @@ async function fetchUsers(): Promise<User[]> { ...@@ -12,33 +15,188 @@ async function fetchUsers(): Promise<User[]> {
} }
} }
interface User { async function fetchNrens(): Promise<Nren[]> {
id: string, try {
email: string, const response = await fetch('/api/nren/list');
roles: string, const userList = await response.json();
active: boolean, return userList
nrens: string[] } catch (error) {
console.log('handle this better..');
return [];
}
} }
const saveUser = (user) => {
const requestOptions = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user),
};
fetch('/api/user', requestOptions)
.then((response) => {
return response.json();
})
.catch((error) => {
const response = error.response;
response.json().then((json) => {
console.log(json);
});
});
};
function UserManagementComponent() { function UserManagementComponent() {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [nrens, setNrens] = useState<Nren[]>([]);
const { user: loggedInUser } = useContext(userContext);
console.log(loggedInUser)
useEffect(() => { useEffect(() => {
// Fetch user // Fetch user
fetchUsers().then((userList) => { fetchUsers().then((userList) => {
setUsers(userList); setUsers(userList);
}); });
fetchNrens().then((nrenList) => {
setNrens(nrenList.sort((a, b) => a.name.localeCompare(b.name)))
})
}, []); }, []);
const handleEdit = (user: User) => {
const index = users.findIndex((u) => u.id === user.id);
const updatedUsers = [...users];
updatedUsers[index].editable = true;
setUsers(updatedUsers);
};
const handleSave = (user: User) => {
const index = users.findIndex((u) => u.id === user.id);
const updatedUsers = [...users];
updatedUsers[index].editable = false;
setUsers(updatedUsers);
// Persist the changes to the server
saveUser(user);
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>, user: User) => {
const index = users.findIndex((u) => u.id === user.id);
const updatedUsers = [...users];
const { name, type } = event.target;
if (type === 'checkbox') {
user[name] = (event.target as HTMLInputElement).checked ? true : false;
} else {
user[name] = (event.target as HTMLInputElement).value;
}
if (event.target.name === 'nrens') {
updatedUsers[index].nrens = [event.target.value];
console.log("updated nren")
}
setUsers(updatedUsers);
};
const findNren = (value) => {
return nrens.find((nren) => {
return nren.id == value || nren.name == value
})?.name
}
return ( return (
<div> <div>
<table> <h1> User Management Page</h1>
{users.map(user => ( <Table>
<tr key={user.id}> <thead>
{user.id} - {user.email} - {user.active ? 'active' : 'inactive'} - {user.roles} - {user.nrens.join()} <tr>
</tr> <th className='pt-3' style={{ border: "1px solid #ddd" }}> Id </th>
))} <th className='pt-3' style={{ border: "1px solid #ddd" }}> Active </th>
</table> <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.permissions.active}
onChange={(event) => handleInputChange(event, user)}
/>
) : ((user.permissions.active ? 'Active' : 'Inactive'))}
</td>
<td style={{ border: "1px dotted #ddd" }}>
{user.editable ? (
<select
name="roles"
value={user.role}
onChange={(event) => handleInputChange(event, user)}>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
) : (
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>
</div> </div>
); );
......
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment