mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 20:40:17 +02:00
[v4.x] ESI Alerts Dashboard Widget
This commit is contained in:
parent
dfcbad3476
commit
ebb40deb7f
@ -1,6 +1,6 @@
|
|||||||
from allianceauth.hooks import DashboardItemHook
|
from allianceauth.hooks import DashboardItemHook
|
||||||
from allianceauth import hooks
|
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):
|
class UserCharactersHook(DashboardItemHook):
|
||||||
@ -26,6 +26,15 @@ class AdminHook(DashboardItemHook):
|
|||||||
DashboardItemHook.__init__(
|
DashboardItemHook.__init__(
|
||||||
self,
|
self,
|
||||||
dashboard_admin,
|
dashboard_admin,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESICheckHook(DashboardItemHook):
|
||||||
|
def __init__(self):
|
||||||
|
DashboardItemHook.__init__(
|
||||||
|
self,
|
||||||
|
dashboard_esi_check,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,3 +52,8 @@ def register_groups_hook():
|
|||||||
@hooks.register('dashboard_hook')
|
@hooks.register('dashboard_hook')
|
||||||
def register_admin_hook():
|
def register_admin_hook():
|
||||||
return AdminHook()
|
return AdminHook()
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('dashboard_hook')
|
||||||
|
def register_esi_hook():
|
||||||
|
return ESICheckHook()
|
||||||
|
12
allianceauth/authentication/constants.py
Normal file
12
allianceauth/authentication/constants.py
Normal file
@ -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")
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
import requests_mock
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import RequestFactory, TestCase
|
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.tests.auth_utils import AuthUtils
|
||||||
|
from allianceauth.authentication.constants import ESI_ERROR_MESSAGE_OVERRIDES
|
||||||
|
|
||||||
MODULE_PATH = "allianceauth.authentication.views"
|
MODULE_PATH = "allianceauth.authentication.views"
|
||||||
|
|
||||||
@ -21,6 +23,8 @@ class TestRunningTasksCount(TestCase):
|
|||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
cls.factory = RequestFactory()
|
cls.factory = RequestFactory()
|
||||||
cls.user = AuthUtils.create_user("bruce_wayne")
|
cls.user = AuthUtils.create_user("bruce_wayne")
|
||||||
|
cls.user.is_superuser = True
|
||||||
|
cls.user.save()
|
||||||
|
|
||||||
def test_should_return_data(
|
def test_should_return_data(
|
||||||
self, mock_active_tasks_count, mock_queued_tasks_count
|
self, mock_active_tasks_count, mock_queued_tasks_count
|
||||||
@ -35,5 +39,164 @@ class TestRunningTasksCount(TestCase):
|
|||||||
# then
|
# then
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertDictEqual(
|
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)
|
||||||
|
@ -40,4 +40,5 @@ urlpatterns = [
|
|||||||
path('dashboard/', views.dashboard, name='dashboard'),
|
path('dashboard/', views.dashboard, name='dashboard'),
|
||||||
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
|
path('dashboard_bs3/', views.dashboard_bs3, name='dashboard_bs3'),
|
||||||
path('task-counts/', views.task_counts, name='task_counts'),
|
path('task-counts/', views.task_counts, name='task_counts'),
|
||||||
|
path('esi-check/', views.esi_check, name='esi_check'),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from allianceauth.hooks import get_hooks
|
||||||
|
|
||||||
from django_registration.backends.activation.views import (
|
from django_registration.backends.activation.views import (
|
||||||
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
REGISTRATION_SALT, ActivationView as BaseActivationView,
|
||||||
@ -9,7 +12,7 @@ from django_registration.signals import user_registered
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import authenticate, login
|
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.contrib.auth.models import User
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
@ -24,6 +27,7 @@ from esi.models import Token
|
|||||||
from allianceauth.eveonline.models import EveCharacter
|
from allianceauth.eveonline.models import EveCharacter
|
||||||
from allianceauth.hooks import get_hooks
|
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 .core.celery_workers import active_tasks_count, queued_tasks_count
|
||||||
from .forms import RegistrationForm
|
from .forms import RegistrationForm
|
||||||
from .models import CharacterOwnership
|
from .models import CharacterOwnership
|
||||||
@ -76,6 +80,13 @@ def dashboard_admin(request):
|
|||||||
return ""
|
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
|
@login_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
_dash_items = list()
|
_dash_items = list()
|
||||||
@ -135,23 +146,28 @@ def token_refresh(request, token_id=None):
|
|||||||
@login_required
|
@login_required
|
||||||
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
|
||||||
def main_character_change(request, token):
|
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:
|
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:
|
except CharacterOwnership.DoesNotExist:
|
||||||
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
|
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
|
||||||
co = CharacterOwnership.objects.create_by_token(token)
|
co = CharacterOwnership.objects.create_by_token(token)
|
||||||
else:
|
else:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
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
|
co = None
|
||||||
if co:
|
if co:
|
||||||
request.user.profile.main_character = co.character
|
request.user.profile.main_character = co.character
|
||||||
request.user.profile.save(update_fields=['main_character'])
|
request.user.profile.save(update_fields=['main_character'])
|
||||||
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
|
messages.success(request, _('Changed main character to %(char)s') % {
|
||||||
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
|
"char": co.character})
|
||||||
|
logger.info('Changed user %(user)s main character to %(char)s' %
|
||||||
|
({'user': request.user, 'char': co.character}))
|
||||||
return redirect("authentication:dashboard")
|
return redirect("authentication:dashboard")
|
||||||
|
|
||||||
|
|
||||||
@ -159,9 +175,11 @@ def main_character_change(request, token):
|
|||||||
def add_character(request, token):
|
def add_character(request, token):
|
||||||
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
|
||||||
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
|
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:
|
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')
|
return redirect('authentication:dashboard')
|
||||||
|
|
||||||
|
|
||||||
@ -278,7 +296,8 @@ class RegistrationView(BaseRegistrationView):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def register(self, form):
|
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.email = form.cleaned_data['email']
|
||||||
user_registered.send(self.__class__, user=user, request=self.request)
|
user_registered.send(self.__class__, user=user, request=self.request)
|
||||||
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
|
||||||
@ -287,7 +306,8 @@ class RegistrationView(BaseRegistrationView):
|
|||||||
else:
|
else:
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
user.save()
|
user.save()
|
||||||
login(self.request, user, 'allianceauth.authentication.backends.StateBackend')
|
login(self.request, user,
|
||||||
|
'allianceauth.authentication.backends.StateBackend')
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def get_activation_key(self, user):
|
def get_activation_key(self, user):
|
||||||
@ -295,7 +315,8 @@ class RegistrationView(BaseRegistrationView):
|
|||||||
|
|
||||||
def get_email_context(self, activation_key):
|
def get_email_context(self, activation_key):
|
||||||
context = super().get_email_context(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
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -328,20 +349,24 @@ class ActivationView(BaseActivationView):
|
|||||||
|
|
||||||
|
|
||||||
def registration_complete(request):
|
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')
|
return redirect('authentication:login')
|
||||||
|
|
||||||
|
|
||||||
def activation_complete(request):
|
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')
|
return redirect('authentication:dashboard')
|
||||||
|
|
||||||
|
|
||||||
def registration_closed(request):
|
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')
|
return redirect('authentication:login')
|
||||||
|
|
||||||
|
|
||||||
|
@user_passes_test(lambda u: u.is_superuser)
|
||||||
def task_counts(request) -> JsonResponse:
|
def task_counts(request) -> JsonResponse:
|
||||||
"""Return task counts as JSON for an AJAX call."""
|
"""Return task counts as JSON for an AJAX call."""
|
||||||
data = {
|
data = {
|
||||||
@ -351,6 +376,25 @@ def task_counts(request) -> JsonResponse:
|
|||||||
return JsonResponse(data)
|
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
|
@login_required
|
||||||
def dashboard_bs3(request):
|
def dashboard_bs3(request):
|
||||||
"""Render dashboard view with BS3 theme.
|
"""Render dashboard view with BS3 theme.
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<div id="esi-alert" class="col-12 align-self-stretch py-2 collapse">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<p class="text-center ">{% translate 'Your Server received an ESI error response code of ' %}<b id="esi-code">?</b></p>
|
||||||
|
<hr>
|
||||||
|
<pre id="esi-data" class="text-center text-wrap"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const elemCard = document.getElementById("esi-alert");
|
||||||
|
const elemMessage = document.getElementById("esi-data");
|
||||||
|
const elemCode = document.getElementById("esi-code");
|
||||||
|
|
||||||
|
fetch('{% url "authentication:esi_check" %}')
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error("Something went wrong");
|
||||||
|
})
|
||||||
|
.then((responseJson) => {
|
||||||
|
console.log("ESI Check: ", JSON.stringify(responseJson, null, 2));
|
||||||
|
|
||||||
|
const status = responseJson.status;
|
||||||
|
if (status != 200) {
|
||||||
|
elemCode.textContent = status
|
||||||
|
elemMessage.textContent = responseJson.data.error;
|
||||||
|
new bootstrap.Collapse(elemCard, {
|
||||||
|
toggle: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user