Compare commits

...

3 Commits

Author SHA1 Message Date
8f40162974 #31 API Tests
* writes test for sharing using the API
* fixes bug on frontend form where an exception occured on generating a new API token if no token existed, yet
* adds permission constraint (default group) for using the api in general
* fixes default-group-only behaviour for sharing-API, so users can only add new users and not removing them, as long as they do not have any other group membership like registration or conservation office
* changes 'ksptoken' to 'Ksptoken' to match CGI standard for http header keys
2022-01-27 14:48:42 +01:00
b86202ba98 #31 API Token mail
* sends an email to the support address if a new token has been generated
2022-01-27 11:47:27 +01:00
b13e67e061 #31 API Frontend token generating
* adds frontend settings for users to create API tokens on their user settings
2022-01-27 11:37:38 +01:00
24 changed files with 534 additions and 43 deletions

View File

@ -5,4 +5,4 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 21.01.22
"""
KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken"
KSP_TOKEN_HEADER_IDENTIFIER = "Ksptoken"

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

7
api/tests/v1/__init__.py Normal file
View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""

View File

@ -0,0 +1,150 @@
import json
from django.urls import reverse
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class APIV1SharingTestCase(BaseTestCase):
@classmethod
def setUpTestData(cls):
""" Creates a test file in memory for using in further tests
Returns:
"""
super().setUpTestData()
cls.superuser.get_API_token()
cls.superuser.api_token.is_active = True
cls.superuser.api_token.save()
default_group = cls.groups.get(name=DEFAULT_GROUP)
cls.superuser.groups.add(default_group)
cls.header_data = {
"HTTP_ksptoken": cls.superuser.api_token.token,
}
def _run_share_request(self, url, user_list: list):
data = {
"users": user_list
}
data = json.dumps(data)
response = self.client.put(
url,
data,
**self.header_data
)
return response
def _test_api_sharing(self, obj, url):
""" Generic test for testing sharing of a ShareableObjectMixin object
Args:
obj (ShareableObjectMixin): The object
url (str): The url to be used for a request
Returns:
"""
self.assertEqual(obj.users.count(), 0)
user_list = [
self.superuser.username,
self.user.username,
]
response = self._run_share_request(url, user_list)
# Must fail, since performing user has no access on requested object
self.assertEqual(response.status_code, 500)
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
# Add performing user to shared access users and rerun the request
obj.users.add(self.superuser)
response = self._run_share_request(url, user_list)
shared_users = obj.shared_users
self.assertEqual(response.status_code, 200)
self.assertEqual(shared_users.count(), 2)
self.assertIn(self.superuser, shared_users)
self.assertIn(self.user, shared_users)
def test_api_token_invalid(self):
""" Tests that a request with an invalid token won't be successfull
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Expect the first request to work properly
self.intervention.users.add(self.superuser)
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
# Change the token
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
# Expect the request to fail now
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 403)
def test_api_intervention_sharing(self):
""" Tests proper sharing of intervention
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
self._test_api_sharing(self.intervention, share_url)
def test_api_eco_account_sharing(self):
""" Tests proper sharing of eco account
Returns:
"""
share_url = reverse("api:v1:ecoaccount-share", args=(self.eco_account.id,))
self._test_api_sharing(self.eco_account, share_url)
def test_api_ema_sharing(self):
""" Tests proper sharing of ema
Returns:
"""
share_url = reverse("api:v1:ema-share", args=(self.ema.id,))
self._test_api_sharing(self.ema, share_url)
def test_api_sharing_as_default_group_only(self):
""" Tests that sharing using the API as an only default group user works as expected.
Expected:
Default only user can only add new users, having shared access. Removing them from the list of users
having shared access is only possible if the user has further rights, e.g. being part of a registration
or conservation office group.
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
self.assertTrue(is_default_group_only(self.superuser))
# Add only him as shared_users an object
self.intervention.users.set([self.superuser])
self.assertEqual(self.intervention.users.count(), 1)
# Try to add another user via API -> must work!
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)
# Now try to remove the user again -> expect no changes at all to the shared user list
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)

View File

@ -7,8 +7,11 @@ Created on: 21.01.22
"""
from django.urls import path, include
from api.views.method_views import generate_new_token_view
app_name = "api"
urlpatterns = [
path("v1/", include("api.urls.v1.urls", namespace="v1")),
path("token/generate", generate_new_token_view, name="generate-new-token"),
]

35
api/views/method_views.py Normal file
View File

@ -0,0 +1,35 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, JsonResponse
from api.models import APIUserToken
@login_required
def generate_new_token_view(request: HttpRequest):
""" Handles request for fetching
Args:
request (HttpRequest): The incoming request
Returns:
"""
if request.method == "GET":
token = APIUserToken()
while APIUserToken.objects.filter(token=token.token).exists():
token = APIUserToken()
return JsonResponse(
data={
"gen_data": token.token
}
)
else:
raise NotImplementedError

View File

@ -18,6 +18,7 @@ from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User
@ -39,6 +40,8 @@ class AbstractAPIView(View):
try:
# Fetch the proper user from the given request header token
self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None))
if not self.user.is_default_user():
raise PermissionError("Default permissions required")
except PermissionError as e:
return self.return_error_response(e, 403)
return super().dispatch(request, *args, **kwargs)
@ -240,6 +243,15 @@ class AbstractModelShareAPIView(AbstractAPIView):
new_users_objs = []
for user in new_users:
new_users_objs.append(User.objects.get(username=user))
if is_default_group_only(self.user):
# Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter(
username__in=new_users
).exclude(
id__in=obj.shared_users
)
new_users_objs = obj.shared_users.union(new_users_to_be_added)
obj.share_with_list(new_users_objs)
return True
@ -253,4 +265,4 @@ class EcoAccountAPIShareView(AbstractModelShareAPIView):
class EmaAPIShareView(AbstractModelShareAPIView):
model = Ema
model = Ema

View File

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
"gen_data": identifier
}
)

View File

@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
"gen_data": identifier
}
)

View File

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
"gen_data": identifier
}
)

View File

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp_intervention.generate_new_identifier()
return JsonResponse(
data={
"identifier": identifier
"gen_data": identifier
}
)

View File

@ -213,6 +213,7 @@ EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
DEFAULT_FROM_EMAIL = "service@ksp.de" # The default email address for the 'from' element
EMAIL_HOST = "localhost"
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
EMAIL_PORT = "25"
#EMAIL_HOST_USER = ""
#EMAIL_HOST_PASSWORD = ""

View File

@ -1,9 +1,9 @@
{% load i18n fontawesome_5 %}
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
<input id="gen-id-input" aria-describedby="gen-id-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
<input id="gen-data-input" aria-describedby="gen-data-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
<div class="input-group-append" onclick="fetchNewIdentifier()">
<span id="gen-id-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
<span id="gen-data-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
</div>
</div>
<script>
@ -13,7 +13,7 @@
return response.json();
})
.then(function(data){
document.getElementById("gen-id-input").value = data["identifier"];
document.getElementById("gen-data-input").value = data["gen_data"];
})
.catch(function(error){
console.log(error);

View File

@ -7,6 +7,7 @@ Created on: 26.10.21
"""
import datetime
from ema.models import Ema
from user.models import User
from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon
@ -52,6 +53,7 @@ class BaseTestCase(TestCase):
cls.intervention = cls.create_dummy_intervention()
cls.compensation = cls.create_dummy_compensation()
cls.eco_account = cls.create_dummy_eco_account()
cls.ema = cls.create_dummy_ema()
cls.create_dummy_states()
cls.create_dummy_action()
cls.codes = cls.create_dummy_codes()
@ -168,6 +170,30 @@ class BaseTestCase(TestCase):
)
return eco_account
@classmethod
def create_dummy_ema(cls):
""" Creates an ema which can be used for tests
Returns:
"""
# Create dummy data
# Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser)
geometry = Geometry.objects.create()
# Create responsible data object
responsible_data = Responsibility.objects.create()
# Finally create main object, holding the other objects
ema = Ema.objects.create(
identifier="TEST",
title="Test_title",
responsible=responsible_data,
created=action,
geometry=geometry,
comment="Test",
)
return ema
@classmethod
def create_dummy_states(cls):
""" Creates an intervention which can be used for tests

View File

@ -11,7 +11,7 @@ from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO, SUPPORT_MAIL_RECIPIENT
logger = logging.getLogger(__name__)
@ -181,3 +181,24 @@ class Mailer:
msg
)
def send_mail_verify_api_token(self, user):
""" Send a mail if a user creates a new token
Args:
user (User): The user, having a new api token
Returns:
"""
context = {
"user": user,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
msg = render_to_string("email/api/verify_token.html", context)
user_mail_address = [SUPPORT_MAIL_RECIPIENT]
self.send(
user_mail_address,
_("Request for new API token"),
msg
)

Binary file not shown.

View File

@ -20,13 +20,13 @@
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:38
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:42
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-20 12:30+0100\n"
"POT-Creation-Date: 2022-01-27 11:44+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -317,6 +317,7 @@ msgid "Identifier"
msgstr "Kennung"
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
#: user/forms.py:126
msgid "Generated automatically"
msgstr "Automatisch generiert"
@ -1716,11 +1717,11 @@ msgstr "Kontrolle am"
msgid "Other"
msgstr "Sonstige"
#: konova/sub_settings/django_settings.py:154
#: konova/sub_settings/django_settings.py:155
msgid "German"
msgstr ""
#: konova/sub_settings/django_settings.py:155
#: konova/sub_settings/django_settings.py:156
msgid "English"
msgstr ""
@ -1803,6 +1804,10 @@ msgstr "{} - Freigegebene Daten gelöscht"
msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:201 templates/email/api/verify_token.html:4
msgid "Request for new API token"
msgstr "Anfrage für neuen API Token"
#: konova/utils/message_templates.py:11
msgid "There was an error on this form."
msgstr "Es gab einen Fehler im Formular."
@ -1937,6 +1942,32 @@ msgstr ""
msgid "Something happened. We are working on it!"
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
#: templates/email/api/verify_token.html:7
msgid "Hello support"
msgstr "Hallo Support"
#: templates/email/api/verify_token.html:9
msgid "you need to verify the API token for user"
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
#: templates/email/api/verify_token.html:13
msgid ""
"If unsure, please contact the user. The API token can not be used until you "
"activated it in the admin backend."
msgstr ""
"Falls Sie sich unsicher sind, kontaktieren Sie den Nutzer vorher. Der API Token kann so lange nicht verwendet werden, "
"wie er noch nicht von Ihnen im Admin Backend aktiviert worden ist."
#: templates/email/api/verify_token.html:16
#: templates/email/checking/shared_data_checked.html:17
#: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/sharing/shared_access_given.html:18
#: templates/email/sharing/shared_access_removed.html:18
msgid "Best regards"
msgstr "Beste Grüße"
#: templates/email/checking/shared_data_checked.html:4
msgid "Shared data checked"
msgstr "Freigegebene Daten geprüft"
@ -1962,15 +1993,6 @@ msgstr ""
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
"Datensatzes soeben bestätigt hat."
#: templates/email/checking/shared_data_checked.html:17
#: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/sharing/shared_access_given.html:18
#: templates/email/sharing/shared_access_removed.html:18
msgid "Best regards"
msgstr "Beste Grüße"
#: templates/email/deleting/shared_data_deleted.html:4
msgid "Shared data deleted"
msgstr "Freigegebene Daten gelöscht"
@ -2214,39 +2236,51 @@ msgid ""
" "
msgstr ""
"\n"
" Diese Daten sind noch nicht veröffentlicht und "
"können daher aktuell nicht eingesehen werden. Schauen Sie zu einem späteren "
"Zeitpunkt wieder vorbei. \n"
" Diese Daten sind noch nicht veröffentlicht und können daher "
"aktuell nicht eingesehen werden. Schauen Sie zu einem späteren Zeitpunkt "
"wieder vorbei. \n"
" "
#: user/forms.py:23
#: user/forms.py:27
msgid "Notifications"
msgstr "Benachrichtigungen"
#: user/forms.py:25
#: user/forms.py:29
msgid "Select the situations when you want to receive a notification"
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
#: user/forms.py:37
#: user/forms.py:41
msgid "Edit notifications"
msgstr "Benachrichtigungen bearbeiten"
#: user/forms.py:72 user/templates/user/index.html:9
#: user/forms.py:76 user/templates/user/index.html:9
msgid "Username"
msgstr "Nutzername"
#: user/forms.py:83
#: user/forms.py:87
msgid "Person name"
msgstr "Name"
#: user/forms.py:94 user/templates/user/index.html:17
#: user/forms.py:98 user/templates/user/index.html:17
msgid "E-Mail"
msgstr ""
#: user/forms.py:108
#: user/forms.py:112
msgid "User contact data"
msgstr "Kontaktdaten"
#: user/forms.py:122
msgid "Token"
msgstr ""
#: user/forms.py:137
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/forms.py:138
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/models/user_action.py:20
msgid "Unrecorded"
msgstr "Entzeichnet"
@ -2300,18 +2334,58 @@ msgstr "Benachrichtigungseinstellungen ändern"
msgid "Notification settings"
msgstr "Benachrichtigungen"
#: user/views.py:29
#: user/templates/user/index.html:58
msgid "See or edit your API token"
msgstr "API token einsehen oder neu generieren"
#: user/templates/user/index.html:61
msgid "API"
msgstr ""
#: user/templates/user/token.html:6
msgid "API settings"
msgstr "API Einstellungen"
#: user/templates/user/token.html:10
msgid "Current token"
msgstr "Aktueller Token"
#: user/templates/user/token.html:14
msgid "Authenticated by admins"
msgstr "Von Admin freigeschaltet"
#: user/templates/user/token.html:16
msgid "Token has been verified and can be used"
msgstr "Token wurde freigeschaltet und kann verwendet werden"
#: user/templates/user/token.html:18
msgid "Token waiting for verification"
msgstr "Token noch nicht freigeschaltet"
#: user/templates/user/token.html:22
msgid "Valid until"
msgstr "Läuft ab am"
#: user/views.py:31
msgid "User settings"
msgstr "Einstellungen"
#: user/views.py:55
#: user/views.py:57
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views.py:67
#: user/views.py:69
msgid "User notifications"
msgstr "Benachrichtigungen"
#: user/views.py:92
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: user/views.py:103
msgid "User API token"
msgstr "API Nutzer Token"
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
@ -3056,7 +3130,7 @@ msgstr ""
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54
msgid "This field is required."
msgstr ""
msgstr "Pflichtfeld"
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
msgid "Enter a whole number."

View File

@ -0,0 +1,23 @@
{% load i18n %}
<div>
<h2>{% trans 'Request for new API token' %}</h2>
<hr>
<article>
{% trans 'Hello support' %},
<br>
{% trans 'you need to verify the API token for user' %}:
<br>
<br>
<strong>{{user.username}}</strong>
<br>
<br>
{% trans 'If unsure, please contact the user. The API token can not be used until you activated it in the admin backend.' %}
<br>
<br>
{% trans 'Best regards' %}
<br>
KSP
</article>
</div>

View File

@ -6,8 +6,12 @@ Created on: 08.07.21
"""
from django import forms
from django.urls import reverse
from django.db import IntegrityError
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from intervention.inputs import GenerateInput
from user.models import User
from konova.forms import BaseForm, BaseModalForm
@ -113,3 +117,46 @@ class UserContactForm(BaseModalForm):
self.initialize_form_field("mail", self.instance.email)
class UserAPITokenForm(BaseForm):
token = forms.CharField(
label=_("Token"),
label_suffix="",
max_length=255,
required=True,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": reverse_lazy("api:generate-new-token"),
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Create new token")
self.form_caption = _("A new token needs to be validated by an administrator!")
self.action_url = reverse("user:api-token")
self.cancel_redirect = reverse("user:index")
# Make direct token editing by user impossible. Instead set the proper url for generating a new token
self.initialize_form_field("token", None)
self.fields["token"].widget.attrs["readonly"] = True
def save(self):
""" Saves the form data
Returns:
api_token (APIUserToken)
"""
user = self.instance
new_token = self.cleaned_data["token"]
if user.api_token is not None:
user.api_token.delete()
new_token = APIUserToken.objects.create(
token=new_token
)
user.api_token = new_token
user.save()
return new_token

View File

@ -9,6 +9,7 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from api.models import APIUserToken
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
from konova.utils.mailer import Mailer
from user.enums import UserNotificationEnum
@ -142,3 +143,19 @@ class User(AbstractUser):
if notification_set:
mailer = Mailer()
mailer.send_mail_shared_data_checked(obj_identifier, self)
def get_API_token(self):
""" Getter for an API token
Creates a new one if none exists, yet.
Returns:
token (APIUserToken)
"""
if self.api_token is None:
token = APIUserToken.objects.create()
self.api_token = token
self.save()
else:
token = self.api_token
return token

View File

@ -54,6 +54,14 @@
</button>
</a>
</div>
<div class="row mb-2">
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
<button class="btn btn-default">
{% fa5_icon 'code' %}
<span>{% trans 'API' %}</span>
</button>
</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% load i18n fontawesome_5 %}
{% block body %}
<div class="container">
<h3>{% trans 'API settings' %}</h3>
<div class="table-container">
<table class="table table-hover">
<tr>
<th scope="row">{% trans 'Current token' %}</th>
<td>{{ user.api_token.token }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Authenticated by admins' %}</th>
{% if user.api_token is None %}
<td></td>
{% elif user.api_token.is_active %}
<td class="text-success" title="{% trans 'Token has been verified and can be used' %}">{% fa5_icon 'check-circle' %}</td>
{% else %}
<td class="text-primary" title="{% trans 'Token waiting for verification' %}">{% fa5_icon 'hourglass-half' %}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans 'Valid until' %}</th>
<td>{{ user.api_token.valid_until|default_if_none:"-" }}</td>
</tr>
</table>
</div>
</div>
<hr>
{% include 'form/table/generic_table_form.html' %}
{% endblock %}

View File

@ -13,6 +13,7 @@ app_name = "user"
urlpatterns = [
path("", index_view, name="index"),
path("notifications/", notifications_view, name="notifications"),
path("token/api", api_token_view, name="api-token"),
path("contact/<id>", contact_view, name="contact"),
]

View File

@ -2,14 +2,16 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID
from user.models import User
from django.http import HttpRequest
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.decorators import any_group_check
from user.forms import UserNotificationForm, UserContactForm
from konova.decorators import any_group_check, default_group_required
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
@login_required
@ -70,6 +72,40 @@ def notifications_view(request: HttpRequest):
return render(request, template, context)
@login_required
@default_group_required
def api_token_view(request: HttpRequest):
""" Handles the request for user api frontend settings
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "user/token.html"
user = request.user
form = UserAPITokenForm(request.POST or None, instance=user)
if request.method == "POST":
if form.is_valid():
token = form.save()
messages.info(request, _("New token generated. Administrators need to validate."))
mailer = Mailer()
mailer.send_mail_verify_api_token(user)
return redirect("user:api-token")
else:
messages.error(request, FORM_INVALID, extra_tags="danger")
elif request.method != "GET":
raise NotImplementedError
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: _("User API token"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def contact_view(request: HttpRequest, id: str):
""" Renders contact modal view of a users contact data