diff --git a/allianceauth/authentication/auth_hooks.py b/allianceauth/authentication/auth_hooks.py index abc22803..b29a3300 100644 --- a/allianceauth/authentication/auth_hooks.py +++ b/allianceauth/authentication/auth_hooks.py @@ -1,6 +1,6 @@ from allianceauth.hooks import DashboardItemHook from allianceauth import hooks -from .views import dashboard_characters, dashboard_groups, dashboard_admin +from .views import dashboard_characters, dashboard_esi_check, dashboard_groups, dashboard_admin class UserCharactersHook(DashboardItemHook): @@ -26,6 +26,15 @@ class AdminHook(DashboardItemHook): DashboardItemHook.__init__( self, dashboard_admin, + 1 + ) + + +class ESICheckHook(DashboardItemHook): + def __init__(self): + DashboardItemHook.__init__( + self, + dashboard_esi_check, 0 ) @@ -43,3 +52,8 @@ def register_groups_hook(): @hooks.register('dashboard_hook') def register_admin_hook(): return AdminHook() + + +@hooks.register('dashboard_hook') +def register_esi_hook(): + return ESICheckHook() diff --git a/allianceauth/authentication/constants.py b/allianceauth/authentication/constants.py new file mode 100644 index 00000000..5a7412e4 --- /dev/null +++ b/allianceauth/authentication/constants.py @@ -0,0 +1,12 @@ +from django.utils.translation import gettext_lazy as _ + +# Overide ESI messages in the dashboard widget +# when the returned messages are not helpful or out of date +ESI_ERROR_MESSAGE_OVERRIDES = { + 420: _("This software has exceeded the error limit for ESI. " + "If you are a user, please contact the maintainer of this software." + " If you are a developer/maintainer, please make a greater " + "effort in the future to receive valid responses. For tips on how, " + "come have a chat with us in ##3rd-party-dev-and-esi on the EVE " + "Online Discord. https://www.eveonline.com/discord") +} diff --git a/allianceauth/authentication/tests/test_views.py b/allianceauth/authentication/tests/test_views.py index 09aa4807..ec7601a9 100644 --- a/allianceauth/authentication/tests/test_views.py +++ b/allianceauth/authentication/tests/test_views.py @@ -1,10 +1,12 @@ import json +import requests_mock from unittest.mock import patch from django.test import RequestFactory, TestCase -from allianceauth.authentication.views import task_counts +from allianceauth.authentication.views import task_counts, esi_check from allianceauth.tests.auth_utils import AuthUtils +from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES MODULE_PATH = "allianceauth.authentication.views" @@ -21,6 +23,8 @@ class TestRunningTasksCount(TestCase): super().setUpClass() cls.factory = RequestFactory() cls.user = AuthUtils.create_user("bruce_wayne") + cls.user.is_superuser = True + cls.user.save() def test_should_return_data( self, mock_active_tasks_count, mock_queued_tasks_count @@ -35,5 +39,164 @@ class TestRunningTasksCount(TestCase): # then self.assertEqual(response.status_code, 200) self.assertDictEqual( - jsonresponse_to_dict(response), {"tasks_running": 2, "tasks_queued": 3} + jsonresponse_to_dict(response), { + "tasks_running": 2, "tasks_queued": 3} ) + + def test_su_only( + self, mock_active_tasks_count, mock_queued_tasks_count + ): + self.user.is_superuser = False + self.user.save() + self.user.refresh_from_db() + # given + mock_active_tasks_count.return_value = 2 + mock_queued_tasks_count.return_value = 3 + request = self.factory.get("/") + request.user = self.user + # when + response = task_counts(request) + # then + self.assertEqual(response.status_code, 302) + + +class TestEsiCheck(TestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.factory = RequestFactory() + cls.user = AuthUtils.create_user("bruce_wayne") + cls.user.is_superuser = True + cls.user.save() + + @requests_mock.Mocker() + def test_401_data_returns_200( + self, m + ): + error_json = { + "error": "You have been banned from using ESI. Please contact Technical Support. (support@eveonline.com)" + } + status_code = 401 + m.get( + "https://esi.evetech.net/latest/status/?datasource=tranquility", + text=json.dumps(error_json), + status_code=status_code + ) + # given + request = self.factory.get("/") + request.user = self.user + # when + response = esi_check(request) + # then + self.assertEqual(response.status_code, 200) + self.assertDictEqual( + jsonresponse_to_dict(response), { + "status": status_code, + "data": error_json + } + ) + + @requests_mock.Mocker() + def test_504_data_returns_200( + self, m + ): + error_json = { + "error": "Gateway timeout message", + "timeout": 5000 + } + status_code = 504 + m.get( + "https://esi.evetech.net/latest/status/?datasource=tranquility", + text=json.dumps(error_json), + status_code=status_code + ) + # given + request = self.factory.get("/") + request.user = self.user + # when + response = esi_check(request) + # then + self.assertEqual(response.status_code, 200) + self.assertDictEqual( + jsonresponse_to_dict(response), { + "status": status_code, + "data": error_json + } + ) + + @requests_mock.Mocker() + def test_420_data_override( + self, m + ): + error_json = { + "error": "message from CCP", + } + status_code = 420 + m.get( + "https://esi.evetech.net/latest/status/?datasource=tranquility", + text=json.dumps(error_json), + status_code=status_code + ) + # given + request = self.factory.get("/") + request.user = self.user + # when + response = esi_check(request) + # then + self.assertEqual(response.status_code, 200) + self.assertNotEqual( + jsonresponse_to_dict(response)["data"], + error_json + ) + self.assertDictEqual( + jsonresponse_to_dict(response), { + "status": status_code, + "data": { + "error": ESI_ERROR_MESSAGE_OVERRIDES.get(status_code) + } + } + ) + + @requests_mock.Mocker() + def test_200_data_returns_200( + self, m + ): + good_json = { + "players": 5, + "server_version": "69420", + "start_time": "2030-01-01T23:59:59Z" + } + status_code = 200 + + m.get( + "https://esi.evetech.net/latest/status/?datasource=tranquility", + text=json.dumps(good_json), + status_code=status_code + ) + # given + request = self.factory.get("/") + request.user = self.user + # when + response = esi_check(request) + # then + self.assertEqual(response.status_code, 200) + self.assertDictEqual( + jsonresponse_to_dict(response), { + "status": status_code, + "data": good_json + } + ) + + def test_su_only( + self, + ): + self.user.is_superuser = False + self.user.save() + self.user.refresh_from_db() + # given + request = self.factory.get("/") + request.user = self.user + # when + response = esi_check(request) + # then + self.assertEqual(response.status_code, 302) diff --git a/allianceauth/authentication/urls.py b/allianceauth/authentication/urls.py index 994fca37..72bc1e10 100644 --- a/allianceauth/authentication/urls.py +++ b/allianceauth/authentication/urls.py @@ -40,4 +40,5 @@ urlpatterns = [ path('dashboard/', views.dashboard, name='dashboard'), path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'), path('task-counts/', views.task_counts, name='task_counts'), + path('esi-check/', views.esi_check, name='esi_check'), ] diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 2ecafa0b..ff66a110 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -1,4 +1,7 @@ import logging +import requests + +from allianceauth.hooks import get_hooks from django_registration.backends.activation.views import ( REGISTRATION_SALT, ActivationView as BaseActivationView, @@ -9,7 +12,7 @@ from django_registration.signals import user_registered from django.conf import settings from django.contrib import messages from django.contrib.auth import authenticate, login -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User from django.core import signing from django.http import JsonResponse @@ -24,6 +27,7 @@ from esi.models import Token from allianceauth.eveonline.models import EveCharacter from allianceauth.hooks import get_hooks +from .constants import ESI_ERROR_MESSAGE_OVERRIDES from .core.celery_workers import active_tasks_count, queued_tasks_count from .forms import RegistrationForm from .models import CharacterOwnership @@ -76,6 +80,13 @@ def dashboard_admin(request): return "" +def dashboard_esi_check(request): + if request.user.is_superuser: + return render_to_string('allianceauth/admin-status/esi_check.html', request=request) + else: + return "" + + @login_required def dashboard(request): _dash_items = list() @@ -135,23 +146,28 @@ def token_refresh(request, token_id=None): @login_required @token_required(scopes=settings.LOGIN_TOKEN_SCOPES) def main_character_change(request, token): - logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}") + logger.debug( + f"main_character_change called by user {request.user} for character {token.character_name}") try: - co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user) + co = CharacterOwnership.objects.get( + character__character_id=token.character_id, user=request.user) except CharacterOwnership.DoesNotExist: if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists(): co = CharacterOwnership.objects.create_by_token(token) else: messages.error( request, - _('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name}) + _('Cannot change main character to %(char)s: character owned by a different account.') % ( + {'char': token.character_name}) ) co = None if co: request.user.profile.main_character = co.character request.user.profile.save(update_fields=['main_character']) - messages.success(request, _('Changed main character to %(char)s') % {"char": co.character}) - logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character})) + messages.success(request, _('Changed main character to %(char)s') % { + "char": co.character}) + logger.info('Changed user %(user)s main character to %(char)s' % + ({'user': request.user, 'char': co.character})) return redirect("authentication:dashboard") @@ -159,9 +175,11 @@ def main_character_change(request, token): def add_character(request, token): if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter( owner_hash=token.character_owner_hash).filter(user=request.user).exists(): - messages.success(request, _('Added %(name)s to your account.' % ({'name': token.character_name}))) + messages.success(request, _( + 'Added %(name)s to your account.' % ({'name': token.character_name}))) else: - messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name}))) + messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ( + {'name': token.character_name}))) return redirect('authentication:dashboard') @@ -278,7 +296,8 @@ class RegistrationView(BaseRegistrationView): return super().dispatch(request, *args, **kwargs) def register(self, form): - user = User.objects.get(pk=self.request.session.get('registration_uid')) + user = User.objects.get( + pk=self.request.session.get('registration_uid')) user.email = form.cleaned_data['email'] user_registered.send(self.__class__, user=user, request=self.request) if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): @@ -287,7 +306,8 @@ class RegistrationView(BaseRegistrationView): else: user.is_active = True user.save() - login(self.request, user, 'allianceauth.authentication.backends.StateBackend') + login(self.request, user, + 'allianceauth.authentication.backends.StateBackend') return user def get_activation_key(self, user): @@ -295,7 +315,8 @@ class RegistrationView(BaseRegistrationView): def get_email_context(self, activation_key): context = super().get_email_context(activation_key) - context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key]) + context['url'] = context['site'].domain + \ + reverse('registration_activate', args=[activation_key]) return context @@ -328,20 +349,24 @@ class ActivationView(BaseActivationView): def registration_complete(request): - messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.')) + messages.success(request, _( + 'Sent confirmation email. Please follow the link to confirm your email address.')) return redirect('authentication:login') def activation_complete(request): - messages.success(request, _('Confirmed your email address. Please login to continue.')) + messages.success(request, _( + 'Confirmed your email address. Please login to continue.')) return redirect('authentication:dashboard') def registration_closed(request): - messages.error(request, _('Registration of new accounts is not allowed at this time.')) + messages.error(request, _( + 'Registration of new accounts is not allowed at this time.')) return redirect('authentication:login') +@user_passes_test(lambda u: u.is_superuser) def task_counts(request) -> JsonResponse: """Return task counts as JSON for an AJAX call.""" data = { @@ -351,6 +376,25 @@ def task_counts(request) -> JsonResponse: return JsonResponse(data) +def check_for_override_esi_error_message(response): + if response.status_code in ESI_ERROR_MESSAGE_OVERRIDES: + return {"error": ESI_ERROR_MESSAGE_OVERRIDES.get(response.status_code)} + else: + return response.json() + + +@user_passes_test(lambda u: u.is_superuser) +def esi_check(request) -> JsonResponse: + """Return if ESI ok With error messages and codes as JSON""" + _r = requests.get("https://esi.evetech.net/latest/status/?datasource=tranquility") + + data = { + "status": _r.status_code, + "data": check_for_override_esi_error_message(_r) + } + return JsonResponse(data) + + @login_required def dashboard_bs3(request): """Render dashboard view with BS3 theme. diff --git a/allianceauth/templates/allianceauth/admin-status/esi_check.html b/allianceauth/templates/allianceauth/admin-status/esi_check.html new file mode 100644 index 00000000..3b8393b9 --- /dev/null +++ b/allianceauth/templates/allianceauth/admin-status/esi_check.html @@ -0,0 +1,37 @@ +{% load i18n %} +
{% translate 'Your Server received an ESI error response code of ' %}?
+