Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into language-settings
Browse files Browse the repository at this point in the history
  • Loading branch information
EmmaVandewalle committed Mar 16, 2024
2 parents f08388d + ef7532c commit 7a30a5a
Show file tree
Hide file tree
Showing 43 changed files with 1,009 additions and 84 deletions.
120 changes: 119 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,120 @@
# UGent-2
[mockup](https://www.figma.com/file/py6Qk9lgFtzbCy9by2qsYU/SELab2?type=design&node-id=617%3A4348&mode=design&t=N4FQR50wAYEyG8qx-1)
De mockup van ons project kan [hier](https://www.figma.com/file/py6Qk9lgFtzbCy9by2qsYU/SELab2?type=design&node-id=617%3A4348&mode=design&t=N4FQR50wAYEyG8qx-1)
gevonden worden.

## Project setup

1. Clone de repository naar je lokale machine met het volgende commando:
```bash
git clone https://github.com/SELab-2/UGent-2
```
## Backend

De backend gebruikt Python 3.12.
Volg deze stappen om de backend van het project op te zetten:


1. Navigeer naar de backend map:
```bash
cd UGent-2/backend
```

2. Start de Python virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate
```
3. Installeer de benodigde Python packages met behulp van het `requirements.txt` bestand:
```bash
pip install -r requirements.txt
```
4. Installeer PostgreSQL:

**Ubuntu**
```bash
sudo apt-get install postgresql postgresql-contrib
```
**Fedora**
```bash
sudo dnf install postgresql postgresql-server
sudo postgresql-setup --initdb --unit postgresql
sudo systemctl enable --now postgresql
```
**Arch**
```bash
sudo pacman -S postgresql
sudo su - postgres -c "initdb --locale $LANG -E UTF8 -D '/var/lib/postgres/data'"
sudo systemctl start postgresql.service
sudo systemctl enable postgresql.service
```
5. Maak een nieuwe database genaamd `delphi` en stel het standaardwachtwoord in:
```bash
sudo -u postgres psql -c "CREATE DATABASE delphi;"
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
```
6. Voer het `fill_database_mock.py` script uit om de database te vullen met mock data:
```bash
python fill_database_mock.py
```
7. Start de API door het `app.py` script uit te voeren:
```bash
python app.py
```
8. Om meer Info te krijgen over de mogelijke requests die je kan maken naar de API, kan je de swagger documentatie raadplegen op de `/api/docs` route.
9. De testen kunnen uitgevoerd worden met het volgende commando:
```bash
python -m unittest discover tests
```

## Frontend

Volg deze stappen om de backend van het project op te zetten:


1. Navigeer naar de backend map:
```bash
cd UGent-2/frontend
```
2. Installeer Node:

**Ubuntu**
```bash
sudo apt update
sudo apt install ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install nodejs
```
**Fedora**
```bash
sudo dnf install nodejs
```
**Arch**
```bash
sudo pacman -S nodejs-lts-iron
```
3. Installeer alle npm dependencies
```bash
npm install
```
4. Build de frontend:
```bash
npm run build
```
De gecompileerde html/css/js bevindt zich nu in de `dist` folder

5. Deploy:

Zet de inhoud van de `dist` folder op de juiste plaats, zodat het geserveerd kan worden.

6. De testen kunnen uitgevoerd worden met: (nog niet geïmplementeerd)
```bash
npm run tests
```

6 changes: 6 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.12-slim
EXPOSE 8000
COPY . /backend
WORKDIR /backend
RUN pip install -r requirements.txt
CMD uvicorn --host 0.0.0.0 app:app
19 changes: 16 additions & 3 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse

from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError, NoSuchRelationError
from routes.errors.authentication import InvalidRoleCredentialsError, NoAccessToSubjectError
from routes.errors.authentication import InvalidAuthenticationError, InvalidRoleCredentialsError, NoAccessToSubjectError
from routes.group import group_router
from routes.login import login_router
from routes.project import project_router
from routes.student import student_router
from routes.subject import subject_router
from routes.teacher import teacher_router
from routes.user import users_router

app = FastAPI()
app = FastAPI(docs_url="/api/docs")

# Koppel routes uit andere modules.
app.include_router(login_router, prefix="/api")
app.include_router(student_router, prefix="/api")
app.include_router(teacher_router, prefix="/api")
app.include_router(users_router, prefix="/api")
app.include_router(project_router, prefix="/api")
app.include_router(subject_router, prefix="/api")
app.include_router(group_router, prefix="/api")

DEBUG = False # Should always be false in repo
DEBUG = False # Should always be false in repo

if DEBUG:
from fastapi.middleware.cors import CORSMiddleware

origins = [
"https://localhost",
"https://localhost:8080",
Expand All @@ -41,6 +45,7 @@
allow_headers=["*"],
)


# Koppel de exception handlers
@app.exception_handler(InvalidRoleCredentialsError)
def invalid_admin_credentials_error_handler(request: Request, exc: InvalidRoleCredentialsError) -> JSONResponse:
Expand Down Expand Up @@ -82,5 +87,13 @@ def no_such_relation_error_handler(request: Request, exc: NoSuchRelationError) -
)


@app.exception_handler(InvalidAuthenticationError)
def invalid_authentication_error_handler(request: Request, exc: NoSuchRelationError) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": str(exc)},
)


if __name__ == "__main__":
uvicorn.run("app:app")
80 changes: 80 additions & 0 deletions backend/controllers/auth/authentication_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import string
from typing import TYPE_CHECKING

import httpx
from defusedxml.ElementTree import fromstring
from sqlalchemy.orm import Session

from domain.logic.student import create_student
from domain.logic.teacher import create_teacher
from domain.logic.user import get_user_with_email
from domain.models.UserDataclass import UserDataclass

if TYPE_CHECKING:
from _elementtree import Element

cas_service = os.getenv("CAS_URL", "https://localhost:8080/login")


def authenticate_user(session: Session, ticket: str) -> UserDataclass | None:
"""
This function will authenticate the user.
If the use doesn't yet exist in the database, it will create an entry.
a
:param session: Session with the database
:param ticket: A ticket from login.ugent.be/login?service=https://localhost:8080/login
:return: None if the authentication failed, user: UseDataclass is the authentication was successful
"""
allowed_chars = set(string.ascii_letters + string.digits + "-")
if not all(c in allowed_chars for c in ticket):
return None
user_information = httpx.get(f"https://login.ugent.be/serviceValidate?service={cas_service}&ticket={ticket}")
user_dict: dict | None = parse_cas_xml(user_information.text)
if user_dict is None:
return None

user: UserDataclass | None = get_user_with_email(session, user_dict["email"])
if user is None:
if user_dict["role"] == "student":
user = create_student(session, user_dict["name"], user_dict["email"])
elif user_dict["role"] == "teacher":
user = create_teacher(session, user_dict["name"], user_dict["email"])
return user


def parse_cas_xml(xml: str) -> dict | None:
"""
The authentication with CAS returns a xml-object.
This function will read the necessary attributes and return them in a dictionary.
:param xml: str: response xml from CAS
:return: None if the authentication failed else dict
"""

namespace = "{http://www.yale.edu/tp/cas}"
root: Element | None = fromstring(xml).find(f"{namespace}authenticationSuccess")
if root is None:
return None
user_information: Element | None = root.find(f"{namespace}attributes")
if user_information is None:
return None
givenname: Element | None = user_information.find(f"{namespace}givenname")
surname: Element | None = user_information.find(f"{namespace}surname")
email: Element | None = user_information.find(f"{namespace}mail")
role: list | None = user_information.findall(f"{namespace}objectClass")
if role is not None and givenname is not None and surname is not None and email is not None:
role_str = ""
for r in role:
if r.text == "ugentStudent" and role_str == "":
role_str = "student"
elif r.text == "ugentEmployee":
role_str = "teacher"

return {
"email": email.text.lower(),
"name": f"{givenname.text} {surname.text}",
"role": role_str,
}
return None
25 changes: 25 additions & 0 deletions backend/controllers/auth/token_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import contextlib
import os
from datetime import UTC, datetime, timedelta

import jwt

from domain.models.UserDataclass import UserDataclass

# Zeker aanpassen in production
jwt_secret = os.getenv("JWT_SECRET", "secret")


def verify_token(token: str) -> int | None:
with contextlib.suppress(jwt.ExpiredSignatureError, jwt.DecodeError):
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
return payload.get("uid", None)


def create_token(user: UserDataclass) -> str:
expire = datetime.now(UTC) + timedelta(days=1)
to_encode: dict = {
"uid": user.id,
"exp": expire,
}
return jwt.encode(to_encode, jwt_secret, algorithm="HS256")
10 changes: 10 additions & 0 deletions backend/db/errors/database_errors.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
class ItemNotFoundError(Exception):
"""
The specified item was not found in the database.
"""
def __init__(self, message: str) -> None:
super().__init__(message)


class ActionAlreadyPerformedError(Exception):
"""
The specified action was already performed on the database once before
and may not be performed again as to keep consistency.
"""
def __init__(self, message: str) -> None:
super().__init__(message)


class NoSuchRelationError(Exception):
"""
There is no relation between the two specified elements in the database.
"""
def __init__(self, message: str) -> None:
super().__init__(message)
19 changes: 18 additions & 1 deletion backend/db/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase

"""
Retrieve the variables needed for a connection to the database.
We use environment variables for the code to be more adaptable across different environments.
The second variable in os.getenv specifies the default value.
"""
# where the database is hosted
db_host = os.getenv("DB_HOST", "localhost")
# port number on which the database server is listening
db_port = os.getenv("DB_PORT", "5432")
# username for the database
db_user = os.getenv("DB_USERNAME", "postgres")
# password for the user
db_password = os.getenv("DB_PASSWORD", "postgres")
# name of the database
db_database = os.getenv("DB_DATABASE", "delphi")

# dialect+driver://username:password@host:port/database
DB_URI = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_database}"

# The engine manages database-operations.
# There is only one instance of the engine, specified here.
engine = create_engine(DB_URI)


class Base(DeclarativeBase):
pass
"""
This class is meant to be inherited from to define the database tables, see [db/models/models.py].
For usage, please check https://docs.sqlalchemy.org/en/20/orm/declarative_styles.html#using-a-declarative-base-class.
"""
14 changes: 12 additions & 2 deletions backend/db/models/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import abstractmethod
from dataclasses import dataclass
from dataclasses import dataclass # automatically add special methods as __init__() and __repr__()
from datetime import datetime
from typing import Generic, TypeVar

Expand All @@ -17,15 +17,25 @@
from domain.models.TeacherDataclass import TeacherDataclass
from domain.models.UserDataclass import UserDataclass

# Create a generic type variable bound to subclasses of BaseModel.
D = TypeVar("D", bound=BaseModel)


@dataclass()
class AbstractModel(Generic[D]):
"""
This class is meant to be inherited by the python classes for the database tables.
It makes sure that every child implements the to_domain_model function
and receives Pydantic data validation.
"""
@abstractmethod
def to_domain_model(self) -> D:
pass
"""
Change to an actual easy-to-use dataclass defined in [domain/models/*].
This prevents working with instances of SQLAlchemy's Base class.
"""

# See the EER diagram for a more visual representation.

@dataclass()
class User(Base, AbstractModel):
Expand Down
Loading

0 comments on commit 7a30a5a

Please sign in to comment.