diff --git a/api/urls/__init__.py b/api/urls/__init__.py new file mode 100644 index 0000000..e4ed550 --- /dev/null +++ b/api/urls/__init__.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import path, include + + +urlpatterns = [ + path("AdonisWebServices/CrewPortalWebService.svc/", include("api.urls.crewportal")), + path("AIWS/AdonisIntegrationWebService.svc/", include("api.urls.integration")), +] diff --git a/api/urls/crewportal.py b/api/urls/crewportal.py new file mode 100644 index 0000000..75988a1 --- /dev/null +++ b/api/urls/crewportal.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.urls import path, include + +from ..views.base import BaseAuthenticationView + + +urlpatterns = [ + path("GNL_API_AUTHENTICATION", BaseAuthenticationView.as_view()), +] \ No newline at end of file diff --git a/api/urls/integration.py b/api/urls/integration.py new file mode 100644 index 0000000..75988a1 --- /dev/null +++ b/api/urls/integration.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.urls import path, include + +from ..views.base import BaseAuthenticationView + + +urlpatterns = [ + path("GNL_API_AUTHENTICATION", BaseAuthenticationView.as_view()), +] \ No newline at end of file diff --git a/api/views.py b/api/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/api/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/api/views/__init__.py b/api/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/views/base.py b/api/views/base.py new file mode 100644 index 0000000..0a60e21 --- /dev/null +++ b/api/views/base.py @@ -0,0 +1,91 @@ +from django.views import View +from django.http import HttpRequest, JsonResponse +from django.utils import timezone + +from datastore.models import APIToken, APIUser + +from typing import List, Optional + +import json + + +class BaseAPIView(View): + http_method_names: List[str] = ["post"] + + unwrap_request: bool = True + wrap_response: bool = True + public: bool = False + + view_name: Optional[str] + data: Optional[dict] + token: Optional[APIToken] + + def get_view_name(self) -> str: + return self.view_name or self.request.get_raw_uri().split("/")[-1] or self.__class__.__name__[:-4] + + def wrapper(self, indict: dict, force: bool = False): + if self.wrap_response or force: + return { + f"{self.get_view_name()}Result": indict + } + + def authentication_error(self, message: str) -> JsonResponse: + response_data: dict = { + "Authentication_Approved": False, + "Authentication_ReasonDenied": message + } + + return JsonResponse(wrapper(response_data)) + + def error(self, message: str) -> dict: + response_data: dict = { + "ErrorText": message + } + + return response_data + + def handle_request(self, request: HttpRequest, *args, **kwargs) -> dict: + raise NotImplementedError( + f"Class {self.__class__.__name__} does not implement handle_request()!") + + def post(self, request: HttpRequest, *args, **kwargs) -> JsonResponse: + self.data: dict = json.loads(request.body) + + if self.unwrap_request: + self.data = self.data["request"] + + if not self.public: + if not self.data["Authentication_Token"]: + return self.authentication_error("Authentication token required for non-public API endpoint") + if not (tokens := APIToken.objects.filter(value=self.data["Authentication_Token"], expiry__lte=timezone.now())).exists(): + return self.authentication_error("Authentication token does not exist or has expired") + + self.token = tokens.first() + + response_data = self.handle_request(request, *args, **kwargs) + response_data["Authentication_Approved"] = True + + return JsonResponse(wrapper(response_data)) + + +class BaseAuthenticationView(BaseAPIView): + unwrap_request = False + wrap_response = False + public = True + + def handle_request(self, request, *args, **kwargs) -> dict: + if (not "credentials" in self.data) or (not "Login" in self.data["credentials"]) or (not "Password" in self.data["credentials"]): + return self.error("You need to pass credentials including Login and Password") + + if (not (users := APIUser.objects.filter(username=self.data["credentials"]["Login"])).exists()) or not (user := users.first()).check_password(self.data["credentials"]["Password"]): + return self.error("Username or password incorrect") + + validity = timezone.timedelta(seconds=int( + self.data["credentials"].get("LifeTime", 0))) + + token = APIToken.objects.create( + user=user, expiry=timezone.now() + validity) + + return { + "Authentication_Token": token.value + } diff --git a/api/views/crewportal/__init__.py b/api/views/crewportal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/views/integration/__init__.py b/api/views/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/datastore/admin.py b/datastore/admin.py index 8c38f3f..f6fcfcb 100644 --- a/datastore/admin.py +++ b/datastore/admin.py @@ -1,3 +1,20 @@ from django.contrib import admin +from django.contrib.auth.models import Group -# Register your models here. +from .models.auth import User, APIUser, APIToken +from .models.crew import CrewMember, Address, Phone, Email +from .models.places import Airport, Country, Nationality, PhoneCode, ZipPlace +from .models.competences import Competence, Role, Certificate +from .forms.auth import APIUserForm + + +for model in [User, APIToken, CrewMember, Address, Phone, Email, Airport, Country, Nationality, PhoneCode, ZipPlace, Competence, Role, Certificate]: + admin.site.register(model) + +class APIUserAdmin(admin.ModelAdmin): + model = APIUser + form = APIUserForm + +admin.site.register(APIUser, APIUserAdmin) + +admin.site.unregister(Group) \ No newline at end of file diff --git a/datastore/forms/auth.py b/datastore/forms/auth.py index 3797671..c3fffbe 100644 --- a/datastore/forms/auth.py +++ b/datastore/forms/auth.py @@ -1,5 +1,5 @@ from django import forms -from django.contrib.auth.hashers import make_password +from django.contrib.auth.hashers import make_password, identify_hasher from django.core.exceptions import ValidationError from ..models.auth import APIUser @@ -15,4 +15,8 @@ class APIUserForm(forms.ModelForm): if not raw: return ValidationError("You did not enter a password.") - return make_password(raw) \ No newline at end of file + try: + identify_hasher(raw) + return make_password(raw) + except: + return raw \ No newline at end of file diff --git a/datastore/migrations/0007_rename_zipplaces_zipplace.py b/datastore/migrations/0007_rename_zipplaces_zipplace.py new file mode 100644 index 0000000..6899a14 --- /dev/null +++ b/datastore/migrations/0007_rename_zipplaces_zipplace.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1 on 2022-08-09 07:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('datastore', '0006_alter_competence_shortname_certificate'), + ] + + operations = [ + migrations.RenameModel( + old_name='ZipPlaces', + new_name='ZipPlace', + ), + ] diff --git a/datastore/migrations/0008_crewmember_profile_picture_and_more.py b/datastore/migrations/0008_crewmember_profile_picture_and_more.py new file mode 100644 index 0000000..a624e2f --- /dev/null +++ b/datastore/migrations/0008_crewmember_profile_picture_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1 on 2022-09-26 06:17 + +import datastore.helpers.uploads +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('datastore', '0007_rename_zipplaces_zipplace'), + ] + + operations = [ + migrations.AddField( + model_name='crewmember', + name='profile_picture', + field=models.ImageField(blank=True, null=True, upload_to=datastore.helpers.uploads.get_upload_path), + ), + migrations.AlterField( + model_name='crewmember', + name='closest_airport', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_first', to='datastore.airport'), + ), + migrations.AlterField( + model_name='crewmember', + name='country_of_birth', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='datastore.country'), + ), + migrations.AlterField( + model_name='crewmember', + name='first_address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_first', to='datastore.address'), + ), + migrations.AlterField( + model_name='crewmember', + name='nationality', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='datastore.nationality'), + ), + migrations.AlterField( + model_name='crewmember', + name='place_of_birth', + field=models.CharField(blank=True, max_length=128, null=True), + ), + ] diff --git a/datastore/migrations/0009_alter_crewmember_closest_airport_and_more.py b/datastore/migrations/0009_alter_crewmember_closest_airport_and_more.py new file mode 100644 index 0000000..2ffe073 --- /dev/null +++ b/datastore/migrations/0009_alter_crewmember_closest_airport_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.1 on 2022-09-26 06:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('datastore', '0008_crewmember_profile_picture_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='crewmember', + name='closest_airport', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_first', to='datastore.airport'), + ), + migrations.AlterField( + model_name='crewmember', + name='competences', + field=models.ManyToManyField(blank=True, null=True, to='datastore.competence'), + ), + migrations.AlterField( + model_name='crewmember', + name='country_of_birth', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='datastore.country'), + ), + migrations.AlterField( + model_name='crewmember', + name='first_address', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_first', to='datastore.address'), + ), + migrations.AlterField( + model_name='crewmember', + name='nationality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='datastore.nationality'), + ), + migrations.AlterField( + model_name='crewmember', + name='roles', + field=models.ManyToManyField(blank=True, null=True, to='datastore.role'), + ), + migrations.AlterField( + model_name='crewmember', + name='second_address', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_second', to='datastore.address'), + ), + migrations.AlterField( + model_name='crewmember', + name='second_airport', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='crew_second', to='datastore.airport'), + ), + ] diff --git a/datastore/migrations/0010_alter_apiuser_username.py b/datastore/migrations/0010_alter_apiuser_username.py new file mode 100644 index 0000000..f2566e9 --- /dev/null +++ b/datastore/migrations/0010_alter_apiuser_username.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2022-09-26 11:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('datastore', '0009_alter_crewmember_closest_airport_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='apiuser', + name='username', + field=models.CharField(max_length=256, unique=True), + ), + ] diff --git a/datastore/migrations/0011_alter_apitoken_value.py b/datastore/migrations/0011_alter_apitoken_value.py new file mode 100644 index 0000000..868cc26 --- /dev/null +++ b/datastore/migrations/0011_alter_apitoken_value.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1 on 2022-09-26 11:03 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('datastore', '0010_alter_apiuser_username'), + ] + + operations = [ + migrations.AlterField( + model_name='apitoken', + name='value', + field=models.UUIDField(default=uuid.uuid4, unique=True), + ), + ] diff --git a/datastore/models/__init__.py b/datastore/models/__init__.py index c6c8a44..d6831f7 100644 --- a/datastore/models/__init__.py +++ b/datastore/models/__init__.py @@ -1,4 +1,4 @@ -from .auth import User -from .crew import CrewMember -from .places import Airport, Country, Nationality, PhoneCode, ZipPlaces +from .auth import User, APIUser, APIToken +from .crew import CrewMember, Address, Phone, Email +from .places import Airport, Country, Nationality, PhoneCode, ZipPlace from .competences import Competence, Role, Certificate \ No newline at end of file diff --git a/datastore/models/auth.py b/datastore/models/auth.py index 2bac116..788bbf8 100644 --- a/datastore/models/auth.py +++ b/datastore/models/auth.py @@ -24,15 +24,18 @@ class User(AbstractBaseUser, PermissionsMixin): class APIUser(models.Model): - username = models.CharField(max_length=256) + username = models.CharField(max_length=256, unique=True) password = models.CharField(max_length=256) def check_password(self, password): return check_password(password, self.password) + def __str__(self): + return self.username + class APIToken(models.Model): - value = models.UUIDField(default=uuid.uuid4) + value = models.UUIDField(default=uuid.uuid4, unique=True) user = models.ForeignKey(APIUser, models.CASCADE) expiry = models.DateTimeField() created_at = models.DateTimeField(auto_now_add=True) diff --git a/datastore/models/crew.py b/datastore/models/crew.py index ca3fcc5..628a72e 100644 --- a/datastore/models/crew.py +++ b/datastore/models/crew.py @@ -1,15 +1,18 @@ from django.db import models -from .places import Nationality, ZipPlaces, Country, Airport, PhoneCode +from .places import Nationality, ZipPlace, Country, Airport, PhoneCode from .competences import Competence, Role +from ..helpers.uploads import get_upload_path + class Address(models.Model): address = models.TextField() - zipplace = models.ForeignKey(ZipPlaces, models.PROTECT) + zipplace = models.ForeignKey(ZipPlace, models.PROTECT) + class CrewMember(models.Model): pin = models.IntegerField(primary_key=True) - + first_name = models.CharField(max_length=128) middle_name = models.CharField(max_length=128, null=True, blank=True) last_name = models.CharField(max_length=128) @@ -18,21 +21,34 @@ class CrewMember(models.Model): title = models.CharField(max_length=128, null=True, blank=True) initials = models.CharField(max_length=16, null=True, blank=True) - nationality = models.ForeignKey(Nationality, models.PROTECT) - place_of_birth = models.CharField(max_length=128) - country_of_birth = models.ForeignKey(Country, models.PROTECT) - first_address = models.ForeignKey(Address, models.PROTECT, related_name="crew_first") - second_address = models.ForeignKey(Address, models.PROTECT, null=True, related_name="crew_second") - closest_airport = models.ForeignKey(Airport, models.PROTECT, related_name="crew_first") - second_airport = models.ForeignKey(Airport, models.PROTECT, null=True, related_name="crew_second") + profile_picture = models.ImageField( + upload_to=get_upload_path, null=True, blank=True) - roles = models.ManyToManyField(Role, null=True) - competences = models.ManyToManyField(Competence, null=True) + nationality = models.ForeignKey( + Nationality, models.PROTECT, null=True, blank=True) + place_of_birth = models.CharField(max_length=128, null=True, blank=True) + country_of_birth = models.ForeignKey( + Country, models.PROTECT, null=True, blank=True) + first_address = models.ForeignKey( + Address, models.PROTECT, related_name="crew_first", null=True, blank=True) + second_address = models.ForeignKey( + Address, models.PROTECT, null=True, related_name="crew_second", blank=True) + closest_airport = models.ForeignKey( + Airport, models.PROTECT, related_name="crew_first", null=True, blank=True) + second_airport = models.ForeignKey( + Airport, models.PROTECT, null=True, related_name="crew_second", blank=True) + + roles = models.ManyToManyField(Role, null=True, blank=True) + competences = models.ManyToManyField(Competence, null=True, blank=True) @property def all_competences(self): return self.competences.all().union(*[role.competences.all() for role in self.roles.all()]) + def __str__(self): + return f"{self.pin}: {self.first_name} {self.last_name}" + + class Phone(models.Model): crew = models.ForeignKey(CrewMember, models.CASCADE) idc = models.ForeignKey(PhoneCode, models.PROTECT, null=True) @@ -40,8 +56,15 @@ class Phone(models.Model): confirmed = models.BooleanField(default=True) priority = models.IntegerField(default=0) + def __str__(self): + return f"{self.crew.pin}: {self.number}" + + class Email(models.Model): crew = models.ForeignKey(CrewMember, models.CASCADE) email = models.EmailField() confirmed = models.BooleanField(default=True) - priority = models.IntegerField(default=0) \ No newline at end of file + priority = models.IntegerField(default=0) + + def __str__(self): + return f"{self.crew.pin}: {self.email}" diff --git a/datastore/models/places.py b/datastore/models/places.py index 39e0715..be79c5d 100644 --- a/datastore/models/places.py +++ b/datastore/models/places.py @@ -8,6 +8,8 @@ from ..helpers.uploads import get_upload_path """ These models are used by Adonis to return tens of thousands of lines of unneeded data. + +Not currently needed, but prepared. """ class Country(models.Model): @@ -23,7 +25,7 @@ class Airport(models.Model): country_name = models.CharField(max_length=256, null=True, blank=True) name = models.CharField(max_length=256, null=True, blank=True) -class ZipPlaces(models.Model): +class ZipPlace(models.Model): postcode = models.CharField(max_length=64) postplace = models.CharField(max_length=256) country = models.ForeignKey(Nationality, models.PROTECT) diff --git a/dumuzid/settings.py b/dumuzid/settings.py index a48f78b..8cb317c 100644 --- a/dumuzid/settings.py +++ b/dumuzid/settings.py @@ -1,5 +1,6 @@ from pathlib import Path +from json import loads from autosecretkey import AutoSecretKey @@ -10,10 +11,15 @@ CONFIG_PATH = BASE_DIR / "settings.ini" ASK = AutoSecretKey(CONFIG_PATH) +SECRET_KEY = ASK.secret_key + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = ASK.config.getboolean("DUMUZID", "Debug", fallback=False) -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] + +if "TrustedOrigins" in ASK.config["DUMUZID"]: + CSRF_TRUSTED_ORIGINS = loads(ASK.config["DUMUZID"]["TrustedOrigins"]) # Application definition @@ -121,10 +127,6 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATICFILES_DIRS = [ - BASE_DIR / "frontend/static/", -] - STATIC_URL = '/static/' STATIC_ROOT = None if DEBUG else ASK.config.get("RESTOROO", "StaticRoot", fallback=BASE_DIR / "static") diff --git a/dumuzid/urls.py b/dumuzid/urls.py index 05897c8..d8cf870 100644 --- a/dumuzid/urls.py +++ b/dumuzid/urls.py @@ -1,21 +1,7 @@ -"""dumuzid URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('', include("api.urls")), ] diff --git a/settings.dist.ini b/settings.dist.ini index 81dec5c..ccc3f82 100644 --- a/settings.dist.ini +++ b/settings.dist.ini @@ -1,5 +1,6 @@ [DUMUZID] Debug = 0 +TrustedOrigins = ["https://dumuzid/"] # [MySQL] # Database = dumuzid