diff --git a/backend/app.py b/backend/app.py index 0eb2e828..d488b4c7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,12 +1,16 @@ import uvicorn from fastapi import FastAPI -from routes.teachers import teachers_router +from routes.projects import projects_router +from routes.subjects import subjects_router +from routes.users import users_router app = FastAPI() # Koppel routes uit andere modules. -app.include_router(teachers_router) +app.include_router(subjects_router, prefix="/api") +app.include_router(users_router, prefix="/api") +app.include_router(projects_router, prefix="/api") if __name__ == "__main__": uvicorn.run("app:app") diff --git a/backend/db/implementation/SqlAdminDAO.py b/backend/db/implementation/SqlAdminDAO.py index 7f1349b7..e9a213f7 100644 --- a/backend/db/implementation/SqlAdminDAO.py +++ b/backend/db/implementation/SqlAdminDAO.py @@ -17,3 +17,8 @@ def create_admin(self, name: str, email: str) -> AdminDataclass: session.add(new_admin) session.commit() return new_admin.to_domain_model() + + def is_user_admin(self, user_id: int) -> bool: + with Session(engine) as session: + admin = session.get(Admin, user_id) + return admin is not None diff --git a/backend/db/implementation/SqlDAOProvider.py b/backend/db/implementation/SqlDAOProvider.py new file mode 100644 index 00000000..e8b2cb9f --- /dev/null +++ b/backend/db/implementation/SqlDAOProvider.py @@ -0,0 +1,43 @@ +from db.implementation.SqlAdminDAO import SqlAdminDAO +from db.implementation.SqlGroupDAO import SqlGroupDAO +from db.implementation.SqlProjectDAO import SqlProjectDAO +from db.implementation.SqlStudentDAO import SqlStudentDAO +from db.implementation.SqlSubjectDAO import SqlSubjectDAO +from db.implementation.SqlSubmissionDAO import SqlSubmissionDAO +from db.implementation.SqlTeacherDAO import SqlTeacherDAO +from db.implementation.SqlUserDAO import SqlUserDAO +from db.interface.AdminDAO import AdminDAO +from db.interface.DAOProvider import DAOProvider +from db.interface.GroupDAO import GroupDAO +from db.interface.ProjectDAO import ProjectDAO +from db.interface.StudentDAO import StudentDAO +from db.interface.SubjectDAO import SubjectDAO +from db.interface.SubmissionDAO import SubmissionDAO +from db.interface.TeacherDAO import TeacherDAO +from db.interface.UserDAO import UserDAO + + +class SqlDAOProvider(DAOProvider): + def get_admin_dao(self) -> AdminDAO: + return SqlAdminDAO() + + def get_group_dao(self) -> GroupDAO: + return SqlGroupDAO() + + def get_project_dao(self) -> ProjectDAO: + return SqlProjectDAO() + + def get_student_dao(self) -> StudentDAO: + return SqlStudentDAO() + + def get_subject_dao(self) -> SubjectDAO: + return SqlSubjectDAO() + + def get_submission_dao(self) -> SubmissionDAO: + return SqlSubmissionDAO() + + def get_teacher_dao(self) -> TeacherDAO: + return SqlTeacherDAO() + + def get_user_dao(self) -> UserDAO: + return SqlUserDAO() diff --git a/backend/db/implementation/SqlStudentDAO.py b/backend/db/implementation/SqlStudentDAO.py index e8556495..2dfd6fe0 100644 --- a/backend/db/implementation/SqlStudentDAO.py +++ b/backend/db/implementation/SqlStudentDAO.py @@ -17,3 +17,8 @@ def create_student(self, name: str, email: str) -> StudentDataclass: session.add(new_student) session.commit() return new_student.to_domain_model() + + def is_user_student(self, user_id: int) -> bool: + with Session(engine) as session: + student = session.get(Student, user_id) + return student is not None diff --git a/backend/db/implementation/SqlTeacherDAO.py b/backend/db/implementation/SqlTeacherDAO.py index 6092e87d..08fd49d4 100644 --- a/backend/db/implementation/SqlTeacherDAO.py +++ b/backend/db/implementation/SqlTeacherDAO.py @@ -17,3 +17,8 @@ def create_teacher(self, name: str, email: str) -> TeacherDataclass: session.add(new_teacher) session.commit() return new_teacher.to_domain_model() + + def is_user_teacher(self, user_id: int) -> bool: + with Session(engine) as session: + teacher = session.get(Teacher, user_id) + return teacher is not None diff --git a/backend/db/interface/AdminDAO.py b/backend/db/interface/AdminDAO.py index b5dac9c6..7b78c652 100644 --- a/backend/db/interface/AdminDAO.py +++ b/backend/db/interface/AdminDAO.py @@ -16,3 +16,7 @@ def create_admin(self, name: str, email: str) -> AdminDataclass: :return: De nieuwe admin """ raise NotImplementedError + + @abstractmethod + def is_user_admin(self, user_id: int) -> bool: + raise NotImplementedError diff --git a/backend/db/interface/DAOProvider.py b/backend/db/interface/DAOProvider.py new file mode 100644 index 00000000..6ebe43c5 --- /dev/null +++ b/backend/db/interface/DAOProvider.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod + +from db.interface.AdminDAO import AdminDAO +from db.interface.GroupDAO import GroupDAO +from db.interface.ProjectDAO import ProjectDAO +from db.interface.StudentDAO import StudentDAO +from db.interface.SubjectDAO import SubjectDAO +from db.interface.SubmissionDAO import SubmissionDAO +from db.interface.TeacherDAO import TeacherDAO +from db.interface.UserDAO import UserDAO + + +class DAOProvider(ABC): + @abstractmethod + def get_admin_dao(self) -> AdminDAO: + raise NotImplementedError + + @abstractmethod + def get_group_dao(self) -> GroupDAO: + raise NotImplementedError + + @abstractmethod + def get_project_dao(self) -> ProjectDAO: + raise NotImplementedError + + @abstractmethod + def get_student_dao(self) -> StudentDAO: + raise NotImplementedError + + @abstractmethod + def get_subject_dao(self) -> SubjectDAO: + raise NotImplementedError + + @abstractmethod + def get_submission_dao(self) -> SubmissionDAO: + raise NotImplementedError + + @abstractmethod + def get_teacher_dao(self) -> TeacherDAO: + raise NotImplementedError + + @abstractmethod + def get_user_dao(self) -> UserDAO: + raise NotImplementedError diff --git a/backend/db/interface/StudentDAO.py b/backend/db/interface/StudentDAO.py index 5bc9deec..e7f9e8da 100644 --- a/backend/db/interface/StudentDAO.py +++ b/backend/db/interface/StudentDAO.py @@ -16,3 +16,7 @@ def create_student(self, name: str, email: str) -> StudentDataclass: :returns: De nieuw aangemaakte student """ raise NotImplementedError + + @abstractmethod + def is_user_student(self, user_id: int) -> bool: + raise NotImplementedError diff --git a/backend/db/interface/SubmissionDAO.py b/backend/db/interface/SubmissionDAO.py index 30356b8f..c5fd33c2 100644 --- a/backend/db/interface/SubmissionDAO.py +++ b/backend/db/interface/SubmissionDAO.py @@ -8,8 +8,14 @@ class SubmissionDAO(AbstractDAO[Submission, SubmissionDataclass]): @abstractmethod - def create_submission(self, student_id: int, group_id: int, message: str, - state: SubmissionState, date_time: datetime) -> SubmissionDataclass: + def create_submission( + self, + student_id: int, + group_id: int, + message: str, + state: SubmissionState, + date_time: datetime, + ) -> SubmissionDataclass: """ Creƫert een nieuw SubmissionDataClass in de database en associeert het met een StudentDataclass en een GroupDataClass. diff --git a/backend/db/interface/TeacherDAO.py b/backend/db/interface/TeacherDAO.py index 925c2906..99bea93c 100644 --- a/backend/db/interface/TeacherDAO.py +++ b/backend/db/interface/TeacherDAO.py @@ -16,3 +16,7 @@ def create_teacher(self, name: str, email: str) -> TeacherDataclass: :returns: De nieuw aangemaakte teacher. """ raise NotImplementedError + + @abstractmethod + def is_user_teacher(self, user_id: int) -> bool: + raise NotImplementedError diff --git a/backend/db/interface/UserDAO.py b/backend/db/interface/UserDAO.py index 84acb923..6769baba 100644 --- a/backend/db/interface/UserDAO.py +++ b/backend/db/interface/UserDAO.py @@ -1,4 +1,3 @@ - from db.interface.AbstractDAO import AbstractDAO from db.models.models import User from domain.models.UserDataclass import UserDataclass diff --git a/backend/domain/logic/SubjectLogic.py b/backend/domain/logic/SubjectLogic.py new file mode 100644 index 00000000..7475ee63 --- /dev/null +++ b/backend/domain/logic/SubjectLogic.py @@ -0,0 +1,17 @@ +from db.interface.DAOProvider import DAOProvider +from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.UserDataclass import UserDataclass + + +def is_user_authorized_for_subject(subject: SubjectDataclass, user: UserDataclass, dao_provider: DAOProvider) -> bool: + teacher_dao = dao_provider.get_teacher_dao() + student_dao = dao_provider.get_student_dao() + subject_dao = dao_provider.get_subject_dao() + + if teacher_dao.is_user_teacher(user.id) and subject in subject_dao.get_subjects_of_teacher(user.id): + return True + + if student_dao.is_user_student(user.id) and subject in subject_dao.get_subjects_of_student(user.id): + return True + + return False diff --git a/backend/domain/logic/UserLogic.py b/backend/domain/logic/UserLogic.py new file mode 100644 index 00000000..428465f6 --- /dev/null +++ b/backend/domain/logic/UserLogic.py @@ -0,0 +1,18 @@ +from db.interface.DAOProvider import DAOProvider +from domain.models.APIUser import APIUser +from domain.models.UserDataclass import UserDataclass + + +def convert_user(user: UserDataclass, dao_provider: DAOProvider) -> APIUser: + api_user = APIUser(id=user.id, name=user.name, email=user.email, roles=[]) + + if dao_provider.get_teacher_dao().is_user_teacher(user.id): + api_user.roles.append("teacher") + + if dao_provider.get_admin_dao().is_user_admin(user.id): + api_user.roles.append("admin") + + if dao_provider.get_student_dao().is_user_student(user.id): + api_user.roles.append("student") + + return api_user diff --git a/backend/domain/models/APIUser.py b/backend/domain/models/APIUser.py new file mode 100644 index 00000000..f1397bfd --- /dev/null +++ b/backend/domain/models/APIUser.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, EmailStr + + +class APIUser(BaseModel): + id: int + name: str + email: EmailStr + roles: list[str] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 762500eb..1539153d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -88,7 +88,9 @@ ignore = [ "TCH001", # Move application import `...` into a type-checking block (dit is enkel voor performance) "RUF009", # Do not perform function call `...` in dataclass defaults but needed for sql alchemy "PLR0913", # Too many arguments in function - "FBT001" # Boolean-typed positional argument in function dfinition + "FBT001", # Boolean-typed positional argument in function dfinition + "FBT002", + "B008" ] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -125,4 +127,4 @@ docstring-code-format = false # # This only has an effect when the `docstring-code-format` setting is # enabled. -docstring-code-line-length = "dynamic" \ No newline at end of file +docstring-code-line-length = "dynamic" diff --git a/backend/routes/db.py b/backend/routes/db.py new file mode 100644 index 00000000..584aedad --- /dev/null +++ b/backend/routes/db.py @@ -0,0 +1,6 @@ +from db.implementation.SqlDAOProvider import SqlDAOProvider +from db.interface.DAOProvider import DAOProvider + + +def get_dao_provider() -> DAOProvider: + return SqlDAOProvider() diff --git a/backend/routes/login.py b/backend/routes/login.py new file mode 100644 index 00000000..d2e32ced --- /dev/null +++ b/backend/routes/login.py @@ -0,0 +1,12 @@ +from domain.models.UserDataclass import UserDataclass +from routes.db import get_dao_provider + + +def get_authenticated_user() -> UserDataclass: + return get_dao_provider().get_user_dao().get(1) # Actually authenticate user + + +def is_user_admin() -> bool: + user = get_authenticated_user() + admin_dao = get_dao_provider().get_admin_dao() + return admin_dao.is_user_admin(user.id) diff --git a/backend/routes/projects.py b/backend/routes/projects.py new file mode 100644 index 00000000..e73b6574 --- /dev/null +++ b/backend/routes/projects.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, HTTPException, status + +from db.errors.database_errors import ItemNotFoundError +from domain.logic.SubjectLogic import is_user_authorized_for_subject +from domain.models.ProjectDataclass import ProjectDataclass +from routes.db import get_dao_provider +from routes.login import get_authenticated_user + +projects_router = APIRouter() + + +@projects_router.get("/projects") +def get_subjects(teacher: bool = False) -> list[ProjectDataclass]: + user = get_authenticated_user() + project_dao = get_dao_provider().get_project_dao() + subject_dao = get_dao_provider().get_subject_dao() + try: + if teacher: + subjects = subject_dao.get_subjects_of_teacher(user.id) + else: + subjects = subject_dao.get_subjects_of_student(user.id) + projects = [] + for i in subjects: + projects += project_dao.get_projects_of_subject(i.id) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err + return projects + + +@projects_router.get("/projects/{project_id}") +def get_project(project_id: int) -> ProjectDataclass: + project_dao = get_dao_provider().get_project_dao() + subject_dao = get_dao_provider().get_subject_dao() + user = get_authenticated_user() + try: + project = project_dao.get(project_id) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err + subject = subject_dao.get(project.subject_id) + if not is_user_authorized_for_subject(subject, user, get_dao_provider()): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return project diff --git a/backend/routes/subjects.py b/backend/routes/subjects.py new file mode 100644 index 00000000..db602a81 --- /dev/null +++ b/backend/routes/subjects.py @@ -0,0 +1,44 @@ +from fastapi import APIRouter, HTTPException, status + +from db.errors.database_errors import ItemNotFoundError +from domain.logic.SubjectLogic import is_user_authorized_for_subject +from domain.models.ProjectDataclass import ProjectDataclass +from domain.models.SubjectDataclass import SubjectDataclass +from routes.db import get_dao_provider +from routes.login import get_authenticated_user + +subjects_router = APIRouter() + + +@subjects_router.get("/subjects") +def get_subjects(teacher: bool = False) -> list[SubjectDataclass]: + user = get_authenticated_user() + subject_dao = get_dao_provider().get_subject_dao() + try: + if teacher: + return subject_dao.get_subjects_of_teacher(user.id) + return subject_dao.get_subjects_of_student(user.id) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err + + +@subjects_router.get("/subjects/{subject_id}") +def get_subject(subject_id: int) -> SubjectDataclass: + subject_dao = get_dao_provider().get_subject_dao() + try: + return subject_dao.get(subject_id) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err + + +@subjects_router.get("/subjects/{subject_id}/projects") +def get_subject_projects(subject_id: int) -> list[ProjectDataclass]: + subject_dao = get_dao_provider().get_subject_dao() + project_dao = get_dao_provider().get_project_dao() + try: + subject = subject_dao.get(subject_id) + if not is_user_authorized_for_subject(subject, get_authenticated_user(), get_dao_provider()): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return project_dao.get_projects_of_subject(subject_id) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err diff --git a/backend/routes/teachers.py b/backend/routes/teachers.py deleted file mode 100644 index c69579b1..00000000 --- a/backend/routes/teachers.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import APIRouter - -from db.implementation.SqlTeacherDAO import SqlTeacherDAO -from db.interface.TeacherDAO import TeacherDAO -from domain.models.TeacherDataclass import TeacherDataclass - -teachers_router = APIRouter() - - -@teachers_router.get("/teachers") -def get_teachers() -> list[TeacherDataclass]: - dao: TeacherDAO = SqlTeacherDAO() - return dao.get_all() - - -@teachers_router.get("/teachers/{teacher_id}") -def get_teacher(teacher_id: int) -> TeacherDataclass: - dao: TeacherDAO = SqlTeacherDAO() - return dao.get(teacher_id) - - -""" -@teachers_router.post("/teachers") -def create_teacher(teacher_data: TeacherDataClassRequest) -> TeacherDataclass: - # can be commented because of the validation that happens through pydantic and FastAPI - # woordjes if not teacher_data: - # woordjes return Response(json.dumps({"error": "Foute JSON of Content-Type"}), status=HTTPStatus.BAD_REQUEST) - - # woordjes validation_result: ValidationResult = TeacherValidator.validate(teacher_data) - # - # woordjes if not validation_result: - # woordjes return Response(json.dumps({"error": validation_result.errors}), status=HTTPStatus.BAD_REQUEST) - - dao: TeacherDAO = SqlTeacherDAO() - # is niet meer nodig omdat teacher_data een instance is van TeacherDataclass - # woordjes lesgever = TeacherDataclass(**teacher_data) # Vul alle velden van het dataobject in met de json - return dao.create_teacher(teacher_data.name, teacher_data.email) -""" diff --git a/backend/routes/users.py b/backend/routes/users.py new file mode 100644 index 00000000..0c08846e --- /dev/null +++ b/backend/routes/users.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, HTTPException, status + +from db.errors.database_errors import ItemNotFoundError +from domain.logic.UserLogic import convert_user +from domain.models.APIUser import APIUser +from routes.db import get_dao_provider +from routes.login import get_authenticated_user, is_user_admin + +users_router = APIRouter() + + +@users_router.get("/user") +def get_current_user() -> APIUser: + user = get_authenticated_user() + return convert_user(user, get_dao_provider()) + + +@users_router.get("/users") +def get_users() -> list[APIUser]: + if not is_user_admin(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + user_dao = get_dao_provider().get_user_dao() + users = user_dao.get_all() + return [convert_user(user, get_dao_provider()) for user in users] + + +@users_router.get("/users/{uid}") +def get_user(uid: int) -> APIUser: + if not is_user_admin(): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + user_dao = get_dao_provider().get_user_dao() + try: + user = user_dao.get(uid) + except ItemNotFoundError as err: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err + return convert_user(user, get_dao_provider())