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

Authentication and state improvements #192

Merged
merged 5 commits into from
Mar 29, 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
2 changes: 1 addition & 1 deletion backend/api/models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def clone(self, clone_assistants=True) -> Self:
)

if clone_assistants:
# Add all the assistants of the current course to the follow up course
# Add all the assistants of the current course to the follow-up course
for assistant in self.assistants.all():
course.assistants.add(assistant)

Expand Down
18 changes: 18 additions & 0 deletions backend/authentication/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from datetime import MINYEAR
from django.db import models
from django.apps import apps
from django.utils.functional import cached_property
from django.db.models import CharField, EmailField, IntegerField, DateTimeField, BooleanField, Model
from django.contrib.auth.models import AbstractBaseUser, AbstractUser, PermissionsMixin

Expand Down Expand Up @@ -38,6 +40,22 @@ def make_admin(self):
self.is_staff = True
self.save()

@cached_property
def roles(self):
"""Return all roles associated with this user"""
return [
# Use the class' name in lower case...
model.__name__.lower()
# ...for each installed app that could define a role model...
for app_config in apps.get_app_configs()
# ...for each model in the app's models...
for model in app_config.get_models()
# ...that inherit the User class.
if model is not self.__class__
if issubclass(model, self.__class__)
if model.objects.filter(id=self.id).exists()
]

@staticmethod
def get_dummy_admin():
return User(
Expand Down
51 changes: 27 additions & 24 deletions backend/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.contrib.auth.models import update_last_login
from rest_framework.serializers import (
CharField,
EmailField,
SerializerMethodField,
HyperlinkedRelatedField,
ModelSerializer,
Serializer,
Expand All @@ -23,7 +23,6 @@ class CASTokenObtainSerializer(Serializer):
This serializer takes the CAS ticket and tries to validate it.
Upon successful validation, create a new user if it doesn't exist.
"""

ticket = CharField(required=True, min_length=49, max_length=49)

def validate(self, data):
Expand Down Expand Up @@ -64,35 +63,37 @@ def _validate_ticket(self, ticket: str) -> dict:
return response.data.get("attributes", dict)

def _fetch_user_from_cas(self, attributes: dict) -> Tuple[User, bool]:
# Convert the lastenrolled attribute
if attributes.get("lastenrolled"):
attributes["lastenrolled"] = int(attributes.get("lastenrolled").split()[0])

user = UserSerializer(
data={
"id": attributes.get("ugentID"),
"username": attributes.get("uid"),
"email": attributes.get("mail"),
"first_name": attributes.get("givenname"),
"last_name": attributes.get("surname"),
"last_enrolled": attributes.get("lastenrolled"),
}
)
# Map the CAS data onto the user data
data = {
"id": attributes.get("ugentID"),
"username": attributes.get("uid"),
"email": attributes.get("mail"),
"first_name": attributes.get("givenname"),
"last_name": attributes.get("surname"),
"last_enrolled": attributes.get("lastenrolled"),
}

# Get or create the user
user, created = User.objects.get_or_create(id=data["id"], defaults=data)

if not user.is_valid():
raise ValidationError(user.errors)
# Validate the serializer
serializer = UserSerializer(user, data=data)

return user.get_or_create(user.validated_data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)

# Save the user
return serializer.save(), created


class UserSerializer(ModelSerializer):
"""Serializer for the user model
This serializer validates the user fields for creation and updating.
"""

id = CharField()
username = CharField()
email = EmailField()

faculties = HyperlinkedRelatedField(
many=True, read_only=True, view_name="faculty-detail"
)
Expand All @@ -102,14 +103,16 @@ class UserSerializer(ModelSerializer):
read_only=True,
)

roles = SerializerMethodField()

def get_roles(self, user: User):
"""Get the roles for the user"""
return user.roles

class Meta:
model = User
fields = "__all__"

def get_or_create(self, validated_data: dict) -> Tuple[User, bool]:
"""Create or fetch the user based on the validated data."""
return User.objects.get_or_create(**validated_data)


class UserIDSerializer(Serializer):
user = PrimaryKeyRelatedField(
Expand Down
16 changes: 6 additions & 10 deletions backend/ypovoli/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.normpath(os.path.join(BASE_DIR, "data/production"))

TESTING_BASE_LINK = "http://testserver"


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
TESTING_BASE_LINK = "http://testserver"

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = environ.get("DJANGO_SECRET_KEY", "lnZZ2xHc6HjU5D85GDE3Nnu4CJsBnm")
Expand All @@ -36,20 +33,19 @@


# Application definition

INSTALLED_APPS = [
# Built-ins
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third party

"rest_framework_swagger", # Swagger
"rest_framework", # Django rest framework
"drf_yasg", # Yet Another Swagger generator
"sslserver", # Used for local SSL support (needed by CAS)
# First party``

"authentication", # Ypovoli authentication
"api", # Ypovoli logic of the base application
"notifications", # Ypovoli notifications
Expand Down Expand Up @@ -85,12 +81,13 @@
"TOKEN_OBTAIN_SERIALIZER": "authentication.serializers.CASTokenObtainSerializer",
}

AUTH_USER_MODEL = "authentication.User"
ROOT_URLCONF = "ypovoli.urls"
WSGI_APPLICATION = "ypovoli.wsgi.application"

# Application endpoints
# Authentication
AUTH_USER_MODEL = "authentication.User"

# Application endpoints
PORT = environ.get("DJANGO_CAS_PORT", "8080")
CAS_ENDPOINT = "https://login.ugent.be"
CAS_RESPONSE = f"https://{DOMAIN_NAME}:{PORT}/auth/verify"
Expand All @@ -114,7 +111,6 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Internationalization

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
Expand Down
89 changes: 89 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"cypress:test": "cypress run"
},
"dependencies": {
"@vueuse/core": "^10.9.0",
"axios": "^1.6.8",
"js-cookie": "^3.0.5",
"moment": "^2.30.1",
Expand Down
53 changes: 34 additions & 19 deletions frontend/src/assets/lang/en.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"layout": {
"header": {
"logo": "University of Ghent logo",
"logo": "Ghent University logo",
"login": "login",
"navigation": {
"dashboard": "Dashboard",
"calendar": "Calendar",
"courses": "Courses",
"settings": "Preferences",
"help": "Help"
"courses": "courses",
"settings": "preferences",
"help": "help"
},
"language": {
"nl": "Dutch",
Expand All @@ -20,6 +21,16 @@
"courses": "My courses",
"projects": "Current projects"
},
"login": {
"title": "Login",
"illustration": "Dare to think",
"subtitle": "Login is done with your UGent account. If you have problems logging in, please contact the administrator.",
"button": "UGent login",
"card": {
"title": "Ypovoli",
"subtitle": "The official submission platform of Ghent University."
}
},
"calendar": {
"title": "Calendar"
},
Expand All @@ -30,30 +41,34 @@
}
},
"composables": {
"helpers": {
"errors": {
"notFound": "Not Found",
"notFoundDetail": "Resource not found.",
"unauthorized": "Unauthorized",
"unauthorizedDetail": "You are not authorized to access this resource.",
"server": "Server Error",
"serverDetail": "An error occurred on the server.",
"network": "Network Error",
"networkDetail": "Unable to connect to the server.",
"request": "Request Error",
"requestDetail": "An error occurred while making the request."
"helpers": {
"errors": {
"notFound": "Not found",
"notFoundDetail": "Source not found",
"unauthorized": "unauthorized",
"unauthorizedDetail": "You are not authorized to access this resource.",
"server": "Server Error",
"serverDetail": "An error occurred on the server.",
"network": "Network Error",
"networkDetail": "Unable to reach the server.",
"request": "request error",
"requestDetail": "An error occurred while creating the request."
}
}
}
},
"components": {
"buttons": {
"academic_year": "Academic year {0}"
},
"card": {
"open": "Details"
"open": "details"
}
},
"toasts": {
"messages": {
"unknown": "An unknown error has occurred."
}
},

"primevue": {
"startsWith": "Starts with",
"contains": "Contains",
Expand Down
Loading
Loading