Implement authentication, etc.

This commit is contained in:
Kumi 2022-09-26 11:48:15 +00:00
parent 8771e425ed
commit 5cfeb46141
Signed by: kumi
GPG key ID: ECBCC9082395383F
22 changed files with 351 additions and 46 deletions

8
api/urls/__init__.py Normal file
View file

@ -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")),
]

9
api/urls/crewportal.py Normal file
View file

@ -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()),
]

9
api/urls/integration.py Normal file
View file

@ -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()),
]

View file

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

0
api/views/__init__.py Normal file
View file

91
api/views/base.py Normal file
View file

@ -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
}

View file

View file

View file

@ -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)

View file

@ -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)
try:
identify_hasher(raw)
return make_password(raw)
except:
return raw

View file

@ -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',
),
]

View file

@ -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),
),
]

View file

@ -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'),
),
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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

View file

@ -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)

View file

@ -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)
priority = models.IntegerField(default=0)
def __str__(self):
return f"{self.crew.pin}: {self.email}"

View file

@ -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)

View file

@ -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")

View file

@ -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")),
]

View file

@ -1,5 +1,6 @@
[DUMUZID]
Debug = 0
TrustedOrigins = ["https://dumuzid/"]
# [MySQL]
# Database = dumuzid