From 32adb64dc0fc57bfa1bcc89c201d5f46c66859f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Mon, 11 Oct 2021 11:33:18 +0200 Subject: [PATCH 01/29] Add api app with ApiToken model --- shynet/api/__init__.py | 0 shynet/api/admin.py | 3 +++ shynet/api/apps.py | 6 ++++++ shynet/api/migrations/0001_initial.py | 30 +++++++++++++++++++++++++++ shynet/api/migrations/__init__.py | 0 shynet/api/models.py | 19 +++++++++++++++++ shynet/api/tests.py | 3 +++ shynet/api/views.py | 3 +++ shynet/shynet/settings.py | 1 + 9 files changed, 65 insertions(+) create mode 100644 shynet/api/__init__.py create mode 100644 shynet/api/admin.py create mode 100644 shynet/api/apps.py create mode 100644 shynet/api/migrations/0001_initial.py create mode 100644 shynet/api/migrations/__init__.py create mode 100644 shynet/api/models.py create mode 100644 shynet/api/tests.py create mode 100644 shynet/api/views.py diff --git a/shynet/api/__init__.py b/shynet/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shynet/api/admin.py b/shynet/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/shynet/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shynet/api/apps.py b/shynet/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/shynet/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py new file mode 100644 index 0000000..b6ebdc0 --- /dev/null +++ b/shynet/api/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.5 on 2021-10-11 09:31 + +import api.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ApiToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64)), + ('value', models.TextField(default=api.models._default_token_value, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['name', 'value'], + }, + ), + ] diff --git a/shynet/api/migrations/__init__.py b/shynet/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shynet/api/models.py b/shynet/api/models.py new file mode 100644 index 0000000..800f726 --- /dev/null +++ b/shynet/api/models.py @@ -0,0 +1,19 @@ +from django.db import models +from core.models import User +from secrets import token_urlsafe + + +def _default_token_value(): + return token_urlsafe(32) + + +class ApiToken(models.Model): + name = models.CharField(max_length=64) + value = models.TextField(default=_default_token_value, unique=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens") + + class Meta: + ordering = ["name", "value"] + + def __str__(self): + return self.name diff --git a/shynet/api/tests.py b/shynet/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shynet/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shynet/api/views.py b/shynet/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/shynet/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py index 709ee75..1774342 100644 --- a/shynet/shynet/settings.py +++ b/shynet/shynet/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ "core", "dashboard.apps.DashboardConfig", "analytics", + "api", "allauth", "allauth.account", "allauth.socialaccount", From bec4b193668c1ccb468f05428f1ba7126c3154ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Mon, 11 Oct 2021 12:37:01 +0200 Subject: [PATCH 02/29] Add ApiToken to admin --- shynet/api/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shynet/api/admin.py b/shynet/api/admin.py index 8c38f3f..4b08f5a 100644 --- a/shynet/api/admin.py +++ b/shynet/api/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin +from api.models import ApiToken -# Register your models here. + +class ApiTokenAdmin(admin.ModelAdmin): + list_display = ("name", "user", "value") + + +admin.site.register(ApiToken, ApiTokenAdmin) From 90b2896ded7926660de8b32d3cefe2b0dedc80bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 16:01:31 +0200 Subject: [PATCH 03/29] Add ApiTokenRequiredMixin --- shynet/api/mixins.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 shynet/api/mixins.py diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py new file mode 100644 index 0000000..380e009 --- /dev/null +++ b/shynet/api/mixins.py @@ -0,0 +1,24 @@ +from django.http import JsonResponse +from django.contrib.auth.models import AnonymousUser +from .models import ApiToken + + +class ApiTokenRequiredMixin: + def _get_user_by_token(self, request): + token = request.headers.get('Authorization') + if not token or not token.startswith('Token '): + return AnonymousUser() + + token = token.split(' ')[1] + api_token = ApiToken.objects.filter(value=token).first() + if not api_token: + return AnonymousUser() + + return api_token.user + + def dispatch(self, request, *args, **kwargs): + request.user = self._get_user_by_token(request) + if not request.user.is_authenticated: + return JsonResponse(data={}, status=403) + + return super().dispatch(request, *args, **kwargs) From a963694fd0958df65ecdd422034076a2123b2004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 19:21:52 +0200 Subject: [PATCH 04/29] Add DashboardApiView --- shynet/api/urls.py | 7 +++++++ shynet/api/views.py | 30 ++++++++++++++++++++++++++++-- shynet/shynet/urls.py | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 shynet/api/urls.py diff --git a/shynet/api/urls.py b/shynet/api/urls.py new file mode 100644 index 0000000..11d877f --- /dev/null +++ b/shynet/api/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("dashboard/", views.DashboardApiView.as_view(), name="services"), +] diff --git a/shynet/api/views.py b/shynet/api/views.py index 91ea44a..e3b5290 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -1,3 +1,29 @@ -from django.shortcuts import render +from django.http import JsonResponse +from django.db.models import Q +from django.db.models.query import QuerySet +from django.views.generic import View -# Create your views here. +from dashboard.mixins import DateRangeMixin +from core.models import Service + +from .mixins import ApiTokenRequiredMixin + + +class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): + def get(self, request, *args, **kwargs): + services = Service.objects.filter( + Q(owner=request.user) | Q(collaborators__in=[request.user]) + ).distinct() + + start = self.get_start_date() + end = self.get_end_date() + services_data = [s.get_core_stats(start, end) for s in services] + for service_data in services_data: + for key, value in service_data.items(): + if isinstance(value, QuerySet): + service_data[key] = list(value) + for key, value in service_data['compare'].items(): + if isinstance(value, QuerySet): + service_data['compare'][key] = list(value) + + return JsonResponse(data={'services': services_data}) diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py index 7264261..71aabe6 100644 --- a/shynet/shynet/urls.py +++ b/shynet/shynet/urls.py @@ -25,4 +25,5 @@ urlpatterns = [ path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")), path("healthz/", include("health_check.urls")), path("", include(("core.urls", "core"), namespace="core")), + path("api/", include(("api.urls", "api"), namespace="api")), ] From 2f8891a8437a924daf261f97c8ae80291dcd3de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 19:52:36 +0200 Subject: [PATCH 05/29] Add minimal argument to get_core_stats --- shynet/api/views.py | 3 ++- shynet/core/models.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index e3b5290..7968ede 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -15,9 +15,10 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): Q(owner=request.user) | Q(collaborators__in=[request.user]) ).distinct() + minimal = request.GET.get('minimal').lower() in ('1', 'true') start = self.get_start_date() end = self.get_end_date() - services_data = [s.get_core_stats(start, end) for s in services] + services_data = [s.get_core_stats(start, end, minimal) for s in services] for service_data in services_data: for key, value in service_data.items(): if isinstance(value, QuerySet): diff --git a/shynet/core/models.py b/shynet/core/models.py index 9568f58..9f78b00 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -107,21 +107,21 @@ class Service(models.Model): start_time=timezone.now() - timezone.timedelta(days=1) ) - def get_core_stats(self, start_time=None, end_time=None): + def get_core_stats(self, start_time=None, end_time=None, minimal=False): if start_time is None: start_time = timezone.now() - timezone.timedelta(days=30) if end_time is None: end_time = timezone.now() - main_data = self.get_relative_stats(start_time, end_time) + main_data = self.get_relative_stats(start_time, end_time, minimal) comparison_data = self.get_relative_stats( - start_time - (end_time - start_time), start_time + start_time - (end_time - start_time), start_time, minimal ) main_data["compare"] = comparison_data return main_data - def get_relative_stats(self, start_time, end_time): + def get_relative_stats(self, start_time, end_time, minimal=False): Session = apps.get_model("analytics", "Session") Hit = apps.get_model("analytics", "Hit") @@ -146,6 +146,28 @@ class Service(models.Model): bounces = sessions.filter(is_bounce=True) bounce_count = bounces.count() + avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ + "load_time__avg" + ] + + avg_hits_per_session = hit_count / session_count if session_count > 0 else None + + avg_session_duration = self._get_avg_session_duration(sessions, session_count) + if minimal: + return { + "currently_online": currently_online, + "session_count": session_count, + "hit_count": hit_count, + "has_hits": has_hits, + "bounce_rate_pct": bounce_count * 100 / session_count + if session_count > 0 + else None, + "avg_session_duration": avg_session_duration, + "avg_load_time": avg_load_time, + "avg_hits_per_session": avg_hits_per_session, + "online": True, + } + locations = ( hits.values("location") .annotate(count=models.Count("location")) @@ -192,17 +214,10 @@ class Service(models.Model): .order_by("-count") ) - avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ - "load_time__avg" - ] - - avg_hits_per_session = hit_count / session_count if session_count > 0 else None - - avg_session_duration = self._get_avg_session_duration(sessions, session_count) - chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data( sessions, hits, start_time, end_time, tz_now ) + return { "currently_online": currently_online, "session_count": session_count, From d62d48c7b402cf80f4c1cb85baa1dd9d6ad4fece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Thu, 14 Oct 2021 07:38:21 +0200 Subject: [PATCH 06/29] Add uuid filter and service uuid filter --- shynet/api/views.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index 7968ede..77b6142 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -15,16 +15,35 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): Q(owner=request.user) | Q(collaborators__in=[request.user]) ).distinct() - minimal = request.GET.get('minimal').lower() in ('1', 'true') + uuid = request.GET.get('uuid') + if uuid: + services = services.filter(uuid=uuid) + + minimal = request.GET.get('minimal', '0').lower() in ('1', 'true') start = self.get_start_date() end = self.get_end_date() - services_data = [s.get_core_stats(start, end, minimal) for s in services] - for service_data in services_data: - for key, value in service_data.items(): - if isinstance(value, QuerySet): - service_data[key] = list(value) - for key, value in service_data['compare'].items(): - if isinstance(value, QuerySet): - service_data['compare'][key] = list(value) + services_data = [ + { + 'name': s.name, + 'uuid': s.uuid, + 'link': s.link, + 'stats': s.get_core_stats(start, end, minimal), + } + for s in services + ] + + if not minimal: + services_data = self._convert_querysets_to_lists(services_data) return JsonResponse(data={'services': services_data}) + + def _convert_querysets_to_lists(self, services_data): + for service_data in services_data: + for key, value in service_data['stats'].items(): + if isinstance(value, QuerySet): + service_data['stats'][key] = list(value) + for key, value in service_data['stats']['compare'].items(): + if isinstance(value, QuerySet): + service_data['stats']['compare'][key] = list(value) + + return service_data From 787ce1775f5bd56991a47eb6218e29afcd93d813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 17 Nov 2021 11:00:52 +0100 Subject: [PATCH 07/29] Move token to User model + add API setting view --- shynet/api/admin.py | 10 +------ shynet/api/migrations/0001_initial.py | 30 ------------------- shynet/api/mixins.py | 9 +++--- shynet/api/models.py | 20 +------------ .../migrations/0009_auto_20211117_0217.py | 24 +++++++++++++++ shynet/core/models.py | 8 ++++- shynet/dashboard/templates/base.html | 3 ++ .../dashboard/pages/api_settings.html | 22 ++++++++++++++ .../dashboard/pages/service_update.html | 7 +++++ shynet/dashboard/urls.py | 14 +++++++-- shynet/dashboard/views.py | 17 +++++++++-- 11 files changed, 94 insertions(+), 70 deletions(-) delete mode 100644 shynet/api/migrations/0001_initial.py create mode 100644 shynet/core/migrations/0009_auto_20211117_0217.py create mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html diff --git a/shynet/api/admin.py b/shynet/api/admin.py index 4b08f5a..d3c1eff 100644 --- a/shynet/api/admin.py +++ b/shynet/api/admin.py @@ -1,9 +1 @@ -from django.contrib import admin -from api.models import ApiToken - - -class ApiTokenAdmin(admin.ModelAdmin): - list_display = ("name", "user", "value") - - -admin.site.register(ApiToken, ApiTokenAdmin) +# from django.contrib import admin diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py deleted file mode 100644 index b6ebdc0..0000000 --- a/shynet/api/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.5 on 2021-10-11 09:31 - -import api.models -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ApiToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=64)), - ('value', models.TextField(default=api.models._default_token_value, unique=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['name', 'value'], - }, - ), - ] diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py index 380e009..47dc9a8 100644 --- a/shynet/api/mixins.py +++ b/shynet/api/mixins.py @@ -1,6 +1,7 @@ from django.http import JsonResponse from django.contrib.auth.models import AnonymousUser -from .models import ApiToken + +from core.models import User class ApiTokenRequiredMixin: @@ -10,11 +11,9 @@ class ApiTokenRequiredMixin: return AnonymousUser() token = token.split(' ')[1] - api_token = ApiToken.objects.filter(value=token).first() - if not api_token: - return AnonymousUser() + user = User.objects.filter(api_token=token).first() - return api_token.user + return user if user else AnonymousUser() def dispatch(self, request, *args, **kwargs): request.user = self._get_user_by_token(request) diff --git a/shynet/api/models.py b/shynet/api/models.py index 800f726..24e1689 100644 --- a/shynet/api/models.py +++ b/shynet/api/models.py @@ -1,19 +1 @@ -from django.db import models -from core.models import User -from secrets import token_urlsafe - - -def _default_token_value(): - return token_urlsafe(32) - - -class ApiToken(models.Model): - name = models.CharField(max_length=64) - value = models.TextField(default=_default_token_value, unique=True) - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens") - - class Meta: - ordering = ["name", "value"] - - def __str__(self): - return self.name +# from django.db import models diff --git a/shynet/core/migrations/0009_auto_20211117_0217.py b/shynet/core/migrations/0009_auto_20211117_0217.py new file mode 100644 index 0000000..461cd1e --- /dev/null +++ b/shynet/core/migrations/0009_auto_20211117_0217.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.5 on 2021-11-17 07:17 + +import core.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_auto_20200628_1403'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='api_token', + field=models.TextField(default=core.models._default_api_token, unique=True), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/shynet/core/models.py b/shynet/core/models.py index 9f78b00..875faba 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -1,8 +1,9 @@ import ipaddress -import json import re import uuid +from secrets import token_urlsafe + from django.apps import apps from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -43,9 +44,14 @@ def _parse_network_list(networks: str): return [ipaddress.ip_network(network.strip()) for network in networks.split(",")] +def _default_api_token(): + return token_urlsafe(32) + + class User(AbstractUser): username = models.TextField(default=_default_uuid, unique=True) email = models.EmailField(unique=True) + api_token = models.TextField(default=_default_api_token, unique=True) def __str__(self): return self.email diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html index f4d2f67..113d602 100644 --- a/shynet/dashboard/templates/base.html +++ b/shynet/dashboard/templates/base.html @@ -88,6 +88,9 @@ {% url 'account_set_password' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %} + {% url 'dashboard:api_settings' as url %} + {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %} + {% url 'account_logout' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %} diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html new file mode 100644 index 0000000..fed6882 --- /dev/null +++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} +
+

API

+
+
+
+

Token

+
+ + {{request.user.api_token}} + + + + +
+
+
+
+
+{% endblock %} diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 097335f..90c56eb 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -30,5 +30,12 @@ +
+
+

API Token

+ + {{request.user.api_token}} + +
{% endblock %} diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py index 328ce58..e107a13 100644 --- a/shynet/dashboard/urls.py +++ b/shynet/dashboard/urls.py @@ -1,6 +1,4 @@ -from django.contrib import admin -from django.urls import include, path -from django.views.generic import RedirectView +from django.urls import path from . import views @@ -28,4 +26,14 @@ urlpatterns = [ views.ServiceSessionView.as_view(), name="service_session", ), + path( + "api-settings/", + views.ApiSettingsView.as_view(), + name="api_settings", + ), + path( + "api-token-refresh/", + views.RefreshApiTokenView.as_view(), + name="api_token_refresh", + ), ] diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 9ce02ce..96a59a8 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -3,8 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.db.models import Q -from django.shortcuts import get_object_or_404, reverse -from django.utils import timezone +from django.shortcuts import get_object_or_404, reverse, redirect from django.views.generic import ( CreateView, DeleteView, @@ -12,11 +11,12 @@ from django.views.generic import ( ListView, TemplateView, UpdateView, + View, ) from rules.contrib.views import PermissionRequiredMixin from analytics.models import Session -from core.models import Service +from core.models import Service, _default_api_token from .forms import ServiceForm from .mixins import DateRangeMixin @@ -155,3 +155,14 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView data = super().get_context_data(**kwargs) data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk")) return data + + +class ApiSettingsView(LoginRequiredMixin, TemplateView): + template_name = "dashboard/pages/api_settings.html" + + +class RefreshApiTokenView(LoginRequiredMixin, View): + def get(self, request): + request.user.api_token = _default_api_token() + request.user.save() + return redirect('dashboard:api_settings') From 8302aedaa793a678c3dc24a6326706839e057e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 17 Nov 2021 11:46:35 +0100 Subject: [PATCH 08/29] Fix problem with whitespaces in copied token --- shynet/dashboard/templates/dashboard/pages/api_settings.html | 4 +--- .../dashboard/templates/dashboard/pages/service_update.html | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html index fed6882..b242766 100644 --- a/shynet/dashboard/templates/dashboard/pages/api_settings.html +++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html @@ -8,9 +8,7 @@

Token

- - {{request.user.api_token}} - + {{request.user.api_token}} diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 90c56eb..c63a176 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -33,9 +33,7 @@

API Token

- - {{request.user.api_token}} - + {{request.user.api_token}}
{% endblock %} From ff6933b4de360d4e72d10e46aef5d210bcb34be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Mon, 11 Oct 2021 11:33:18 +0200 Subject: [PATCH 09/29] Add api app with ApiToken model --- shynet/api/__init__.py | 0 shynet/api/admin.py | 3 +++ shynet/api/apps.py | 6 ++++++ shynet/api/migrations/0001_initial.py | 30 +++++++++++++++++++++++++++ shynet/api/migrations/__init__.py | 0 shynet/api/models.py | 19 +++++++++++++++++ shynet/api/tests.py | 3 +++ shynet/api/views.py | 3 +++ shynet/shynet/settings.py | 1 + 9 files changed, 65 insertions(+) create mode 100644 shynet/api/__init__.py create mode 100644 shynet/api/admin.py create mode 100644 shynet/api/apps.py create mode 100644 shynet/api/migrations/0001_initial.py create mode 100644 shynet/api/migrations/__init__.py create mode 100644 shynet/api/models.py create mode 100644 shynet/api/tests.py create mode 100644 shynet/api/views.py diff --git a/shynet/api/__init__.py b/shynet/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shynet/api/admin.py b/shynet/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/shynet/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shynet/api/apps.py b/shynet/api/apps.py new file mode 100644 index 0000000..66656fd --- /dev/null +++ b/shynet/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api' diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py new file mode 100644 index 0000000..b6ebdc0 --- /dev/null +++ b/shynet/api/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.5 on 2021-10-11 09:31 + +import api.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ApiToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64)), + ('value', models.TextField(default=api.models._default_token_value, unique=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['name', 'value'], + }, + ), + ] diff --git a/shynet/api/migrations/__init__.py b/shynet/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shynet/api/models.py b/shynet/api/models.py new file mode 100644 index 0000000..800f726 --- /dev/null +++ b/shynet/api/models.py @@ -0,0 +1,19 @@ +from django.db import models +from core.models import User +from secrets import token_urlsafe + + +def _default_token_value(): + return token_urlsafe(32) + + +class ApiToken(models.Model): + name = models.CharField(max_length=64) + value = models.TextField(default=_default_token_value, unique=True) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens") + + class Meta: + ordering = ["name", "value"] + + def __str__(self): + return self.name diff --git a/shynet/api/tests.py b/shynet/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/shynet/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shynet/api/views.py b/shynet/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/shynet/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py index e461481..9232d65 100644 --- a/shynet/shynet/settings.py +++ b/shynet/shynet/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ "core", "dashboard.apps.DashboardConfig", "analytics", + "api", "allauth", "allauth.account", "allauth.socialaccount", From 1dec03c724dd5c7129ebd51025989b8ac026a9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Mon, 11 Oct 2021 12:37:01 +0200 Subject: [PATCH 10/29] Add ApiToken to admin --- shynet/api/admin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shynet/api/admin.py b/shynet/api/admin.py index 8c38f3f..4b08f5a 100644 --- a/shynet/api/admin.py +++ b/shynet/api/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin +from api.models import ApiToken -# Register your models here. + +class ApiTokenAdmin(admin.ModelAdmin): + list_display = ("name", "user", "value") + + +admin.site.register(ApiToken, ApiTokenAdmin) From a7248cd54b7f4979e0cf21e8ebbdf16f7f36d4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 16:01:31 +0200 Subject: [PATCH 11/29] Add ApiTokenRequiredMixin --- shynet/api/mixins.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 shynet/api/mixins.py diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py new file mode 100644 index 0000000..380e009 --- /dev/null +++ b/shynet/api/mixins.py @@ -0,0 +1,24 @@ +from django.http import JsonResponse +from django.contrib.auth.models import AnonymousUser +from .models import ApiToken + + +class ApiTokenRequiredMixin: + def _get_user_by_token(self, request): + token = request.headers.get('Authorization') + if not token or not token.startswith('Token '): + return AnonymousUser() + + token = token.split(' ')[1] + api_token = ApiToken.objects.filter(value=token).first() + if not api_token: + return AnonymousUser() + + return api_token.user + + def dispatch(self, request, *args, **kwargs): + request.user = self._get_user_by_token(request) + if not request.user.is_authenticated: + return JsonResponse(data={}, status=403) + + return super().dispatch(request, *args, **kwargs) From 5966ea2f84c38e33697e441cd2f95d94f00d945e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 19:21:52 +0200 Subject: [PATCH 12/29] Add DashboardApiView --- shynet/api/urls.py | 7 +++++++ shynet/api/views.py | 30 ++++++++++++++++++++++++++++-- shynet/shynet/urls.py | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 shynet/api/urls.py diff --git a/shynet/api/urls.py b/shynet/api/urls.py new file mode 100644 index 0000000..11d877f --- /dev/null +++ b/shynet/api/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("dashboard/", views.DashboardApiView.as_view(), name="services"), +] diff --git a/shynet/api/views.py b/shynet/api/views.py index 91ea44a..e3b5290 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -1,3 +1,29 @@ -from django.shortcuts import render +from django.http import JsonResponse +from django.db.models import Q +from django.db.models.query import QuerySet +from django.views.generic import View -# Create your views here. +from dashboard.mixins import DateRangeMixin +from core.models import Service + +from .mixins import ApiTokenRequiredMixin + + +class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): + def get(self, request, *args, **kwargs): + services = Service.objects.filter( + Q(owner=request.user) | Q(collaborators__in=[request.user]) + ).distinct() + + start = self.get_start_date() + end = self.get_end_date() + services_data = [s.get_core_stats(start, end) for s in services] + for service_data in services_data: + for key, value in service_data.items(): + if isinstance(value, QuerySet): + service_data[key] = list(value) + for key, value in service_data['compare'].items(): + if isinstance(value, QuerySet): + service_data['compare'][key] = list(value) + + return JsonResponse(data={'services': services_data}) diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py index 7264261..71aabe6 100644 --- a/shynet/shynet/urls.py +++ b/shynet/shynet/urls.py @@ -25,4 +25,5 @@ urlpatterns = [ path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")), path("healthz/", include("health_check.urls")), path("", include(("core.urls", "core"), namespace="core")), + path("api/", include(("api.urls", "api"), namespace="api")), ] From e577aa4997bcc693e9e98ca160aff946e3dbf965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 13 Oct 2021 19:52:36 +0200 Subject: [PATCH 13/29] Add minimal argument to get_core_stats --- shynet/api/views.py | 3 ++- shynet/core/models.py | 39 +++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index e3b5290..7968ede 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -15,9 +15,10 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): Q(owner=request.user) | Q(collaborators__in=[request.user]) ).distinct() + minimal = request.GET.get('minimal').lower() in ('1', 'true') start = self.get_start_date() end = self.get_end_date() - services_data = [s.get_core_stats(start, end) for s in services] + services_data = [s.get_core_stats(start, end, minimal) for s in services] for service_data in services_data: for key, value in service_data.items(): if isinstance(value, QuerySet): diff --git a/shynet/core/models.py b/shynet/core/models.py index 9568f58..9f78b00 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -107,21 +107,21 @@ class Service(models.Model): start_time=timezone.now() - timezone.timedelta(days=1) ) - def get_core_stats(self, start_time=None, end_time=None): + def get_core_stats(self, start_time=None, end_time=None, minimal=False): if start_time is None: start_time = timezone.now() - timezone.timedelta(days=30) if end_time is None: end_time = timezone.now() - main_data = self.get_relative_stats(start_time, end_time) + main_data = self.get_relative_stats(start_time, end_time, minimal) comparison_data = self.get_relative_stats( - start_time - (end_time - start_time), start_time + start_time - (end_time - start_time), start_time, minimal ) main_data["compare"] = comparison_data return main_data - def get_relative_stats(self, start_time, end_time): + def get_relative_stats(self, start_time, end_time, minimal=False): Session = apps.get_model("analytics", "Session") Hit = apps.get_model("analytics", "Hit") @@ -146,6 +146,28 @@ class Service(models.Model): bounces = sessions.filter(is_bounce=True) bounce_count = bounces.count() + avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ + "load_time__avg" + ] + + avg_hits_per_session = hit_count / session_count if session_count > 0 else None + + avg_session_duration = self._get_avg_session_duration(sessions, session_count) + if minimal: + return { + "currently_online": currently_online, + "session_count": session_count, + "hit_count": hit_count, + "has_hits": has_hits, + "bounce_rate_pct": bounce_count * 100 / session_count + if session_count > 0 + else None, + "avg_session_duration": avg_session_duration, + "avg_load_time": avg_load_time, + "avg_hits_per_session": avg_hits_per_session, + "online": True, + } + locations = ( hits.values("location") .annotate(count=models.Count("location")) @@ -192,17 +214,10 @@ class Service(models.Model): .order_by("-count") ) - avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ - "load_time__avg" - ] - - avg_hits_per_session = hit_count / session_count if session_count > 0 else None - - avg_session_duration = self._get_avg_session_duration(sessions, session_count) - chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data( sessions, hits, start_time, end_time, tz_now ) + return { "currently_online": currently_online, "session_count": session_count, From d809ec82d9888618f8f7a74f920409233552f4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Thu, 14 Oct 2021 07:38:21 +0200 Subject: [PATCH 14/29] Add uuid filter and service uuid filter --- shynet/api/views.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index 7968ede..77b6142 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -15,16 +15,35 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): Q(owner=request.user) | Q(collaborators__in=[request.user]) ).distinct() - minimal = request.GET.get('minimal').lower() in ('1', 'true') + uuid = request.GET.get('uuid') + if uuid: + services = services.filter(uuid=uuid) + + minimal = request.GET.get('minimal', '0').lower() in ('1', 'true') start = self.get_start_date() end = self.get_end_date() - services_data = [s.get_core_stats(start, end, minimal) for s in services] - for service_data in services_data: - for key, value in service_data.items(): - if isinstance(value, QuerySet): - service_data[key] = list(value) - for key, value in service_data['compare'].items(): - if isinstance(value, QuerySet): - service_data['compare'][key] = list(value) + services_data = [ + { + 'name': s.name, + 'uuid': s.uuid, + 'link': s.link, + 'stats': s.get_core_stats(start, end, minimal), + } + for s in services + ] + + if not minimal: + services_data = self._convert_querysets_to_lists(services_data) return JsonResponse(data={'services': services_data}) + + def _convert_querysets_to_lists(self, services_data): + for service_data in services_data: + for key, value in service_data['stats'].items(): + if isinstance(value, QuerySet): + service_data['stats'][key] = list(value) + for key, value in service_data['stats']['compare'].items(): + if isinstance(value, QuerySet): + service_data['stats']['compare'][key] = list(value) + + return service_data From 66b841fd86a398e3a738ece90d74ea6f5b11f83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 17 Nov 2021 11:00:52 +0100 Subject: [PATCH 15/29] Move token to User model + add API setting view --- shynet/api/admin.py | 10 +------ shynet/api/migrations/0001_initial.py | 30 ------------------- shynet/api/mixins.py | 9 +++--- shynet/api/models.py | 20 +------------ .../migrations/0009_auto_20211117_0217.py | 24 +++++++++++++++ shynet/core/models.py | 8 ++++- shynet/dashboard/templates/base.html | 3 ++ .../dashboard/pages/api_settings.html | 22 ++++++++++++++ .../dashboard/pages/service_update.html | 7 +++++ shynet/dashboard/urls.py | 14 +++++++-- shynet/dashboard/views.py | 17 +++++++++-- 11 files changed, 94 insertions(+), 70 deletions(-) delete mode 100644 shynet/api/migrations/0001_initial.py create mode 100644 shynet/core/migrations/0009_auto_20211117_0217.py create mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html diff --git a/shynet/api/admin.py b/shynet/api/admin.py index 4b08f5a..d3c1eff 100644 --- a/shynet/api/admin.py +++ b/shynet/api/admin.py @@ -1,9 +1 @@ -from django.contrib import admin -from api.models import ApiToken - - -class ApiTokenAdmin(admin.ModelAdmin): - list_display = ("name", "user", "value") - - -admin.site.register(ApiToken, ApiTokenAdmin) +# from django.contrib import admin diff --git a/shynet/api/migrations/0001_initial.py b/shynet/api/migrations/0001_initial.py deleted file mode 100644 index b6ebdc0..0000000 --- a/shynet/api/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.5 on 2021-10-11 09:31 - -import api.models -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ApiToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=64)), - ('value', models.TextField(default=api.models._default_token_value, unique=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['name', 'value'], - }, - ), - ] diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py index 380e009..47dc9a8 100644 --- a/shynet/api/mixins.py +++ b/shynet/api/mixins.py @@ -1,6 +1,7 @@ from django.http import JsonResponse from django.contrib.auth.models import AnonymousUser -from .models import ApiToken + +from core.models import User class ApiTokenRequiredMixin: @@ -10,11 +11,9 @@ class ApiTokenRequiredMixin: return AnonymousUser() token = token.split(' ')[1] - api_token = ApiToken.objects.filter(value=token).first() - if not api_token: - return AnonymousUser() + user = User.objects.filter(api_token=token).first() - return api_token.user + return user if user else AnonymousUser() def dispatch(self, request, *args, **kwargs): request.user = self._get_user_by_token(request) diff --git a/shynet/api/models.py b/shynet/api/models.py index 800f726..24e1689 100644 --- a/shynet/api/models.py +++ b/shynet/api/models.py @@ -1,19 +1 @@ -from django.db import models -from core.models import User -from secrets import token_urlsafe - - -def _default_token_value(): - return token_urlsafe(32) - - -class ApiToken(models.Model): - name = models.CharField(max_length=64) - value = models.TextField(default=_default_token_value, unique=True) - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="api_tokens") - - class Meta: - ordering = ["name", "value"] - - def __str__(self): - return self.name +# from django.db import models diff --git a/shynet/core/migrations/0009_auto_20211117_0217.py b/shynet/core/migrations/0009_auto_20211117_0217.py new file mode 100644 index 0000000..461cd1e --- /dev/null +++ b/shynet/core/migrations/0009_auto_20211117_0217.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.5 on 2021-11-17 07:17 + +import core.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_auto_20200628_1403'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='api_token', + field=models.TextField(default=core.models._default_api_token, unique=True), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/shynet/core/models.py b/shynet/core/models.py index 9f78b00..875faba 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -1,8 +1,9 @@ import ipaddress -import json import re import uuid +from secrets import token_urlsafe + from django.apps import apps from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -43,9 +44,14 @@ def _parse_network_list(networks: str): return [ipaddress.ip_network(network.strip()) for network in networks.split(",")] +def _default_api_token(): + return token_urlsafe(32) + + class User(AbstractUser): username = models.TextField(default=_default_uuid, unique=True) email = models.EmailField(unique=True) + api_token = models.TextField(default=_default_api_token, unique=True) def __str__(self): return self.email diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html index f4d2f67..113d602 100644 --- a/shynet/dashboard/templates/base.html +++ b/shynet/dashboard/templates/base.html @@ -88,6 +88,9 @@ {% url 'account_set_password' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %} + {% url 'dashboard:api_settings' as url %} + {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %} + {% url 'account_logout' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %} diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html new file mode 100644 index 0000000..fed6882 --- /dev/null +++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} +
+

API

+
+
+
+

Token

+
+ + {{request.user.api_token}} + + + + +
+
+
+
+
+{% endblock %} diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 9036de8..7a9713b 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -30,5 +30,12 @@
+
+
+

API Token

+ + {{request.user.api_token}} + +
{% endblock %} diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py index 328ce58..e107a13 100644 --- a/shynet/dashboard/urls.py +++ b/shynet/dashboard/urls.py @@ -1,6 +1,4 @@ -from django.contrib import admin -from django.urls import include, path -from django.views.generic import RedirectView +from django.urls import path from . import views @@ -28,4 +26,14 @@ urlpatterns = [ views.ServiceSessionView.as_view(), name="service_session", ), + path( + "api-settings/", + views.ApiSettingsView.as_view(), + name="api_settings", + ), + path( + "api-token-refresh/", + views.RefreshApiTokenView.as_view(), + name="api_token_refresh", + ), ] diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 9ce02ce..96a59a8 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -3,8 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.db.models import Q -from django.shortcuts import get_object_or_404, reverse -from django.utils import timezone +from django.shortcuts import get_object_or_404, reverse, redirect from django.views.generic import ( CreateView, DeleteView, @@ -12,11 +11,12 @@ from django.views.generic import ( ListView, TemplateView, UpdateView, + View, ) from rules.contrib.views import PermissionRequiredMixin from analytics.models import Session -from core.models import Service +from core.models import Service, _default_api_token from .forms import ServiceForm from .mixins import DateRangeMixin @@ -155,3 +155,14 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView data = super().get_context_data(**kwargs) data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk")) return data + + +class ApiSettingsView(LoginRequiredMixin, TemplateView): + template_name = "dashboard/pages/api_settings.html" + + +class RefreshApiTokenView(LoginRequiredMixin, View): + def get(self, request): + request.user.api_token = _default_api_token() + request.user.save() + return redirect('dashboard:api_settings') From bcf94147c92c1331e1c10846bb9c68277025fb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 17 Nov 2021 11:46:35 +0100 Subject: [PATCH 16/29] Fix problem with whitespaces in copied token --- shynet/dashboard/templates/dashboard/pages/api_settings.html | 4 +--- .../dashboard/templates/dashboard/pages/service_update.html | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html index fed6882..b242766 100644 --- a/shynet/dashboard/templates/dashboard/pages/api_settings.html +++ b/shynet/dashboard/templates/dashboard/pages/api_settings.html @@ -8,9 +8,7 @@

Token

- - {{request.user.api_token}} - + {{request.user.api_token}} diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 7a9713b..010db09 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -33,9 +33,7 @@

API Token

- - {{request.user.api_token}} - + {{request.user.api_token}}
{% endblock %} From 069b218828892d84213e9bf9edc1b30c1f805aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Tue, 4 Jan 2022 08:52:39 +0100 Subject: [PATCH 17/29] Move api token info to security tab --- .../templates/account/password_change.html | 15 ++++++++++++-- shynet/dashboard/templates/base.html | 3 --- .../dashboard/pages/api_settings.html | 20 ------------------- shynet/dashboard/views.py | 2 +- 4 files changed, 14 insertions(+), 26 deletions(-) delete mode 100644 shynet/dashboard/templates/dashboard/pages/api_settings.html diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html index ca8f276..2a68a1d 100644 --- a/shynet/dashboard/templates/account/password_change.html +++ b/shynet/dashboard/templates/account/password_change.html @@ -2,8 +2,8 @@ {% load i18n a17t_tags %} -{% block head_title %}{% trans "Change Password" %}{% endblock %} -{% block page_title %}{% trans "Change Password" %}{% endblock %} +{% block head_title %}{% trans "Change authentication info" %}{% endblock %} +{% block page_title %}{% trans "Change authentication info" %}{% endblock %} {% block card %}
@@ -11,4 +11,15 @@ {{ form|a17t }}
+
+
+

Personal API token

+
+ {{request.user.api_token}} + + + +
+
+
{% endblock %} diff --git a/shynet/dashboard/templates/base.html b/shynet/dashboard/templates/base.html index 113d602..f4d2f67 100644 --- a/shynet/dashboard/templates/base.html +++ b/shynet/dashboard/templates/base.html @@ -88,9 +88,6 @@ {% url 'account_set_password' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Security" url=url %} - {% url 'dashboard:api_settings' as url %} - {% include 'dashboard/includes/sidebar_portal.html' with label="API" url=url %} - {% url 'account_logout' as url %} {% include 'dashboard/includes/sidebar_portal.html' with label="Sign Out" url=url %} diff --git a/shynet/dashboard/templates/dashboard/pages/api_settings.html b/shynet/dashboard/templates/dashboard/pages/api_settings.html deleted file mode 100644 index b242766..0000000 --- a/shynet/dashboard/templates/dashboard/pages/api_settings.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-

API

-
-
-
-

Token

-
- {{request.user.api_token}} - - - -
-
-
-
-
-{% endblock %} diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 96a59a8..080e05d 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -165,4 +165,4 @@ class RefreshApiTokenView(LoginRequiredMixin, View): def get(self, request): request.user.api_token = _default_api_token() request.user.save() - return redirect('dashboard:api_settings') + return redirect('account_change_password') From 7f60b3abff5ec2f44bc28b7307db22c87a85b13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 5 Jan 2022 08:53:46 +0100 Subject: [PATCH 18/29] Rename minimal parameter to basic --- shynet/api/views.py | 6 +++--- shynet/core/models.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index 77b6142..cd0181e 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -19,7 +19,7 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): if uuid: services = services.filter(uuid=uuid) - minimal = request.GET.get('minimal', '0').lower() in ('1', 'true') + basic = request.GET.get('basic', '0').lower() in ('1', 'true') start = self.get_start_date() end = self.get_end_date() services_data = [ @@ -27,12 +27,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): 'name': s.name, 'uuid': s.uuid, 'link': s.link, - 'stats': s.get_core_stats(start, end, minimal), + 'stats': s.get_core_stats(start, end, basic), } for s in services ] - if not minimal: + if not basic: services_data = self._convert_querysets_to_lists(services_data) return JsonResponse(data={'services': services_data}) diff --git a/shynet/core/models.py b/shynet/core/models.py index 875faba..426c8f8 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -113,21 +113,21 @@ class Service(models.Model): start_time=timezone.now() - timezone.timedelta(days=1) ) - def get_core_stats(self, start_time=None, end_time=None, minimal=False): + def get_core_stats(self, start_time=None, end_time=None, basic=False): if start_time is None: start_time = timezone.now() - timezone.timedelta(days=30) if end_time is None: end_time = timezone.now() - main_data = self.get_relative_stats(start_time, end_time, minimal) + main_data = self.get_relative_stats(start_time, end_time, basic) comparison_data = self.get_relative_stats( - start_time - (end_time - start_time), start_time, minimal + start_time - (end_time - start_time), start_time, basic ) main_data["compare"] = comparison_data return main_data - def get_relative_stats(self, start_time, end_time, minimal=False): + def get_relative_stats(self, start_time, end_time, basic=False): Session = apps.get_model("analytics", "Session") Hit = apps.get_model("analytics", "Hit") @@ -159,7 +159,7 @@ class Service(models.Model): avg_hits_per_session = hit_count / session_count if session_count > 0 else None avg_session_duration = self._get_avg_session_duration(sessions, session_count) - if minimal: + if basic: return { "currently_online": currently_online, "session_count": session_count, From 2aaadfe81cc3587f52a4832490ad39e1e5937450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 5 Jan 2022 09:02:23 +0100 Subject: [PATCH 19/29] Display api urls on service management page --- .../dashboard/pages/service_update.html | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 010db09..03ff1c0 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -32,8 +32,21 @@
-

API Token

- {{request.user.api_token}} +
API
+

Service data can be accessed via API on url:

+ {{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}} +

+ There are 3 optional query parameters: +

    +
  • startDate - to set start date in format YYYY-MM-DD
  • +
  • endDate - to set end date in format YYYY-MM-DD
  • +
  • basic - to get only basic data set to '1' or 'true'
  • +
+

+

Example using HTTPie:

+ http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{request.user.api_token}}' +

Example using cURL:

+ curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'
{% endblock %} From ba91ed561d733bea8598300ad917c208053e4f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 5 Jan 2022 09:47:14 +0100 Subject: [PATCH 20/29] Add uuid validation --- shynet/api/views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index cd0181e..bad7dae 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -1,3 +1,4 @@ +import uuid from django.http import JsonResponse from django.db.models import Q from django.db.models.query import QuerySet @@ -9,6 +10,14 @@ from core.models import Service from .mixins import ApiTokenRequiredMixin +def is_valid_uuid(value): + try: + uuid.UUID(value) + return True + except ValueError: + return False + + class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): def get(self, request, *args, **kwargs): services = Service.objects.filter( @@ -16,7 +25,7 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): ).distinct() uuid = request.GET.get('uuid') - if uuid: + if uuid and is_valid_uuid(uuid): services = services.filter(uuid=uuid) basic = request.GET.get('basic', '0').lower() in ('1', 'true') From 6d84f6313081de7c58a1a6b0f0d49d24f7c1c3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Wed, 5 Jan 2022 10:27:14 +0100 Subject: [PATCH 21/29] Add API documentation to GUIDE.md --- GUIDE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/GUIDE.md b/GUIDE.md index 90153ed..40e2916 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -208,6 +208,25 @@ In a single-page application, the page never reloads. (That's the entire point o Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go. + +### API + +All data displayed on dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. +By default this url return full data from all services from last 30 days. +Authentication header should be set to use user's parsonal API token (```'Authorization:Token '```). + +There are 4 optional query parameters: + * uuid - to get data only from one service + * startDate - to set start date in format YYYY-MM-DD + * endDate - to set end date in format YYYY-MM-DD + * basic - to get only basic data set to '1' or 'true' + +Example in HTTPie: +```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'``` + +Example in cURL: +```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'``` + --- ## Troubleshooting From 4a6af187651f27e8898febf4206ac754ba6b3795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Thu, 14 Apr 2022 19:41:14 +0200 Subject: [PATCH 22/29] Add django-cors-headers --- poetry.lock | 17 ++++++++++++++++- pyproject.toml | 1 + shynet/shynet/settings.py | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 42436dc..0e41f22 100644 --- a/poetry.lock +++ b/poetry.lock @@ -300,6 +300,17 @@ python3-openid = ">=3.0.8" requests = "*" requests-oauthlib = ">=0.3.0" +[[package]] +name = "django-cors-headers" +version = "3.11.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Django = ">=2.2" + [[package]] name = "django-coverage-plugin" version = "2.0.2" @@ -949,7 +960,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "009655b041e17f83ac3f1b423241ee299b0e706274b5b77519d3c087a4a032f8" +content-hash = "9fa33a531809239cfa88d8d896484af7d8be765b2a0f4878ab5434d5af6b30d2" [metadata.files] aiohttp = [ @@ -1220,6 +1231,10 @@ django = [ django-allauth = [ {file = "django-allauth-0.45.0.tar.gz", hash = "sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"}, ] +django-cors-headers = [ + {file = "django-cors-headers-3.11.0.tar.gz", hash = "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"}, + {file = "django_cors_headers-3.11.0-py3-none-any.whl", hash = "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b"}, +] django-coverage-plugin = [ {file = "django_coverage_plugin-2.0.2-py3-none-any.whl", hash = "sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952"}, {file = "django_coverage_plugin-2.0.2.tar.gz", hash = "sha256:e91e3a0c8de2b3766a144cdd30dbbf7a79e5c532a5dcc1373ce7eaad83b358b3"}, diff --git a/pyproject.toml b/pyproject.toml index e32781b..00564c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ django-health-check = "^3.16.4" django-npm = "^1.0.0" python-dotenv = "^0.18.0" django-debug-toolbar = "^3.2.1" +django-cors-headers = "^3.11.0" [tool.poetry.dev-dependencies] pytest-sugar = "^0.9.4" diff --git a/shynet/shynet/settings.py b/shynet/shynet/settings.py index ec0ade3..976e1d9 100644 --- a/shynet/shynet/settings.py +++ b/shynet/shynet/settings.py @@ -64,12 +64,14 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "debug_toolbar", + "corsheaders", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -371,3 +373,6 @@ DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5")) USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = ( os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True" ) + +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_METHODS = ["GET", "OPTIONS"] From b87b158aabb8e82da68652740d164f6d8be6bbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Fri, 22 Apr 2022 08:28:09 +0200 Subject: [PATCH 23/29] Fix typo --- shynet/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index bad7dae..0cea315 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -55,4 +55,4 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): if isinstance(value, QuerySet): service_data['stats']['compare'][key] = list(value) - return service_data + return services_data From ca97453c3e431482a346be0488a89dd0a8037720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Tue, 26 Apr 2022 10:13:52 +0200 Subject: [PATCH 24/29] Return 400 if date format is invalid --- shynet/api/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index 0cea315..5ee4900 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -29,8 +29,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): services = services.filter(uuid=uuid) basic = request.GET.get('basic', '0').lower() in ('1', 'true') - start = self.get_start_date() - end = self.get_end_date() + try: + start = self.get_start_date() + end = self.get_end_date() + except ValueError: + return JsonResponse(status=400, data={'error': 'Invalid date format'}) + services_data = [ { 'name': s.name, From d9bbeea89278b814e6b5faad14a3d9102e2686bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Thu, 12 May 2022 12:10:44 +0200 Subject: [PATCH 25/29] Remove basic option from API For simplicity --- shynet/core/models.py | 38 ++++++------------- .../dashboard/pages/service_update.html | 7 ++-- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/shynet/core/models.py b/shynet/core/models.py index 426c8f8..dac7e60 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -113,21 +113,21 @@ class Service(models.Model): start_time=timezone.now() - timezone.timedelta(days=1) ) - def get_core_stats(self, start_time=None, end_time=None, basic=False): + def get_core_stats(self, start_time=None, end_time=None): if start_time is None: start_time = timezone.now() - timezone.timedelta(days=30) if end_time is None: end_time = timezone.now() - main_data = self.get_relative_stats(start_time, end_time, basic) + main_data = self.get_relative_stats(start_time, end_time) comparison_data = self.get_relative_stats( - start_time - (end_time - start_time), start_time, basic + start_time - (end_time - start_time), start_time ) main_data["compare"] = comparison_data return main_data - def get_relative_stats(self, start_time, end_time, basic=False): + def get_relative_stats(self, start_time, end_time): Session = apps.get_model("analytics", "Session") Hit = apps.get_model("analytics", "Hit") @@ -152,28 +152,6 @@ class Service(models.Model): bounces = sessions.filter(is_bounce=True) bounce_count = bounces.count() - avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ - "load_time__avg" - ] - - avg_hits_per_session = hit_count / session_count if session_count > 0 else None - - avg_session_duration = self._get_avg_session_duration(sessions, session_count) - if basic: - return { - "currently_online": currently_online, - "session_count": session_count, - "hit_count": hit_count, - "has_hits": has_hits, - "bounce_rate_pct": bounce_count * 100 / session_count - if session_count > 0 - else None, - "avg_session_duration": avg_session_duration, - "avg_load_time": avg_load_time, - "avg_hits_per_session": avg_hits_per_session, - "online": True, - } - locations = ( hits.values("location") .annotate(count=models.Count("location")) @@ -220,6 +198,14 @@ class Service(models.Model): .order_by("-count") ) + avg_load_time = hits.aggregate(load_time__avg=models.Avg("load_time"))[ + "load_time__avg" + ] + + avg_hits_per_session = hit_count / session_count if session_count > 0 else None + + avg_session_duration = self._get_avg_session_duration(sessions, session_count) + chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data( sessions, hits, start_time, end_time, tz_now ) diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 03ff1c0..67ba6e5 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -36,17 +36,16 @@

Service data can be accessed via API on url:

{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}

- There are 3 optional query parameters: + There are 2 optional query parameters:

  • startDate - to set start date in format YYYY-MM-DD
  • endDate - to set end date in format YYYY-MM-DD
  • -
  • basic - to get only basic data set to '1' or 'true'

Example using HTTPie:

- http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{request.user.api_token}}' + http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{request.user.api_token}}'

Example using cURL:

- curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' + curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' {% endblock %} From 77cb1fb37c0da5bad39b3905f7a48cd3f176bac7 Mon Sep 17 00:00:00 2001 From: "R. Miles McCain" Date: Sat, 27 Aug 2022 14:52:02 -0700 Subject: [PATCH 26/29] Improve language --- GUIDE.md | 12 +++++----- .../templates/account/password_change.html | 5 +++-- .../dashboard/pages/service_update.html | 22 +++++++++---------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 40e2916..be2ef7f 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -211,15 +211,13 @@ Fortunately, Shynet offers a simple method you can call from anywhere within you ### API -All data displayed on dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. -By default this url return full data from all services from last 30 days. -Authentication header should be set to use user's parsonal API token (```'Authorization:Token '```). +All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```). There are 4 optional query parameters: - * uuid - to get data only from one service - * startDate - to set start date in format YYYY-MM-DD - * endDate - to set end date in format YYYY-MM-DD - * basic - to get only basic data set to '1' or 'true' + * `uuid` - to get data only from one service + * `startDate` - to set start date in format YYYY-MM-DD + * `endDate` - to set end date in format YYYY-MM-DD + * `basic` - to get only basic data set to '1' or 'true' Example in HTTPie: ```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'``` diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html index 2a68a1d..6bb4a6e 100644 --- a/shynet/dashboard/templates/account/password_change.html +++ b/shynet/dashboard/templates/account/password_change.html @@ -13,13 +13,14 @@
-

Personal API token

+

Personal API token

{{request.user.api_token}} - +
+

To learn more about the API, see our API guide.

{% endblock %} diff --git a/shynet/dashboard/templates/dashboard/pages/service_update.html b/shynet/dashboard/templates/dashboard/pages/service_update.html index 67ba6e5..aeb2c58 100644 --- a/shynet/dashboard/templates/dashboard/pages/service_update.html +++ b/shynet/dashboard/templates/dashboard/pages/service_update.html @@ -31,21 +31,19 @@
-
-
API
-

Service data can be accessed via API on url:

- {{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}} +
API
+
+

Shynet provides a simple API that you can use to pull data programmatically. You can access this data via this URL:

+ {{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}

- There are 2 optional query parameters: -

    -
  • startDate - to set start date in format YYYY-MM-DD
  • -
  • endDate - to set end date in format YYYY-MM-DD
  • -
+ There are 2 optional query parameters: +
    +
  • startDate — to set the start date (in format YYYY-MM-DD)
  • +
  • endDate — to set the end date (in format YYYY-MM-DD)
  • +

-

Example using HTTPie:

- http get '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{request.user.api_token}}'

Example using cURL:

- curl -H 'Authorization:Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01' + curl -H 'Authorization: Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'
{% endblock %} From b7f2e9cfe656be1860264704d48e89c9429205f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Thu, 12 May 2022 12:40:33 +0200 Subject: [PATCH 27/29] Remove basic option from api view --- shynet/api/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/shynet/api/views.py b/shynet/api/views.py index 5ee4900..a5dc895 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -28,7 +28,6 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): if uuid and is_valid_uuid(uuid): services = services.filter(uuid=uuid) - basic = request.GET.get('basic', '0').lower() in ('1', 'true') try: start = self.get_start_date() end = self.get_end_date() @@ -40,13 +39,12 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): 'name': s.name, 'uuid': s.uuid, 'link': s.link, - 'stats': s.get_core_stats(start, end, basic), + 'stats': s.get_core_stats(start, end), } for s in services ] - if not basic: - services_data = self._convert_querysets_to_lists(services_data) + services_data = self._convert_querysets_to_lists(services_data) return JsonResponse(data={'services': services_data}) From b286c80754e7973a9487c3f218205780777546b0 Mon Sep 17 00:00:00 2001 From: "R. Miles McCain" Date: Sun, 28 Aug 2022 15:07:05 -0700 Subject: [PATCH 28/29] Remove unneeded views --- GUIDE.md | 9 ++++----- shynet/dashboard/urls.py | 5 ----- shynet/dashboard/views.py | 4 ---- shynet/shynet/urls.py | 2 +- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index be2ef7f..2b67ce3 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -211,19 +211,18 @@ Fortunately, Shynet offers a simple method you can call from anywhere within you ### API -All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```). +All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/v1/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token '```). -There are 4 optional query parameters: +There are 3 optional query parameters: * `uuid` - to get data only from one service * `startDate` - to set start date in format YYYY-MM-DD * `endDate` - to set end date in format YYYY-MM-DD - * `basic` - to get only basic data set to '1' or 'true' Example in HTTPie: -```http get '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1' 'Authorization:Token {{user_api_token}}'``` +```http get '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{user_api_token}}'``` Example in cURL: -```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01&basic=1'``` +```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01'``` --- diff --git a/shynet/dashboard/urls.py b/shynet/dashboard/urls.py index e107a13..7aa917b 100644 --- a/shynet/dashboard/urls.py +++ b/shynet/dashboard/urls.py @@ -26,11 +26,6 @@ urlpatterns = [ views.ServiceSessionView.as_view(), name="service_session", ), - path( - "api-settings/", - views.ApiSettingsView.as_view(), - name="api_settings", - ), path( "api-token-refresh/", views.RefreshApiTokenView.as_view(), diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 080e05d..8039430 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -157,10 +157,6 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView return data -class ApiSettingsView(LoginRequiredMixin, TemplateView): - template_name = "dashboard/pages/api_settings.html" - - class RefreshApiTokenView(LoginRequiredMixin, View): def get(self, request): request.user.api_token = _default_api_token() diff --git a/shynet/shynet/urls.py b/shynet/shynet/urls.py index 71aabe6..cbec472 100644 --- a/shynet/shynet/urls.py +++ b/shynet/shynet/urls.py @@ -25,5 +25,5 @@ urlpatterns = [ path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")), path("healthz/", include("health_check.urls")), path("", include(("core.urls", "core"), namespace="core")), - path("api/", include(("api.urls", "api"), namespace="api")), + path("api/v1/", include(("api.urls", "api"), namespace="api")), ] From 5e48e2dcf54c080607da962eb2d79de8b3bcf0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Jastrz=C4=99bski?= Date: Mon, 29 Aug 2022 08:44:17 +0200 Subject: [PATCH 29/29] Use POST to api token refresh --- shynet/dashboard/templates/account/password_change.html | 7 ++++--- shynet/dashboard/views.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/shynet/dashboard/templates/account/password_change.html b/shynet/dashboard/templates/account/password_change.html index 6bb4a6e..3fe2bbf 100644 --- a/shynet/dashboard/templates/account/password_change.html +++ b/shynet/dashboard/templates/account/password_change.html @@ -16,9 +16,10 @@

Personal API token

{{request.user.api_token}} - - - +
+ {% csrf_token %} + +

To learn more about the API, see our API guide.

diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 8039430..97360d0 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -158,7 +158,7 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView class RefreshApiTokenView(LoginRequiredMixin, View): - def get(self, request): + def post(self, request): request.user.api_token = _default_api_token() request.user.save() return redirect('account_change_password')