Implement authentication, etc.
This commit is contained in:
parent
8771e425ed
commit
5cfeb46141
22 changed files with 351 additions and 46 deletions
8
api/urls/__init__.py
Normal file
8
api/urls/__init__.py
Normal 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
9
api/urls/crewportal.py
Normal 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
9
api/urls/integration.py
Normal 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()),
|
||||
]
|
|
@ -1,3 +0,0 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
api/views/__init__.py
Normal file
0
api/views/__init__.py
Normal file
91
api/views/base.py
Normal file
91
api/views/base.py
Normal 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
|
||||
}
|
0
api/views/crewportal/__init__.py
Normal file
0
api/views/crewportal/__init__.py
Normal file
0
api/views/integration/__init__.py
Normal file
0
api/views/integration/__init__.py
Normal 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)
|
|
@ -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
|
17
datastore/migrations/0007_rename_zipplaces_zipplace.py
Normal file
17
datastore/migrations/0007_rename_zipplaces_zipplace.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
18
datastore/migrations/0010_alter_apiuser_username.py
Normal file
18
datastore/migrations/0010_alter_apiuser_username.py
Normal 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),
|
||||
),
|
||||
]
|
19
datastore/migrations/0011_alter_apitoken_value.py
Normal file
19
datastore/migrations/0011_alter_apitoken_value.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")),
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[DUMUZID]
|
||||
Debug = 0
|
||||
TrustedOrigins = ["https://dumuzid/"]
|
||||
|
||||
# [MySQL]
|
||||
# Database = dumuzid
|
||||
|
|
Loading…
Reference in a new issue