Polish analytics & add collaboration

This commit is contained in:
R. Miles McCain 2020-04-14 09:51:34 -04:00
parent e486397c9d
commit cfe3dac408
No known key found for this signature in database
GPG Key ID: 91CB47BDDF2671A5
15 changed files with 132 additions and 70 deletions

View File

@ -6,6 +6,16 @@
{{ field }} {{ field }}
<span>{{ field.label }}</span> <span>{{ field.label }}</span>
</label> </label>
{% elif field|is_multiple_checkbox %}
{% if field.auto_id %}
<label class="label my-1" for="{{field.auto_id}}">{{ field.label }}</label>
{% endif %}
{% for choice in field %}
<label class="switch my-1 block">
{{ choice.tag }}
<span>{{ choice.choice_label }}</span>
</label>
{% endfor %}
{% elif field|is_radio %} {% elif field|is_radio %}
{% if field.auto_id %} {% if field.auto_id %}
<label class="label my-1" for="{{field.auto_id}}">{{ field.label }}</label> <label class="label my-1" for="{{field.auto_id}}">{{ field.label }}</label>

View File

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
name = "core" name = "core"
# def ready(self):
# import core.rules

View File

@ -7,10 +7,11 @@ from .models import Service
class ServiceForm(forms.ModelForm): class ServiceForm(forms.ModelForm):
class Meta: class Meta:
model = Service model = Service
fields = ["name", "link", "origins"] fields = ["name", "link", "origins", "collaborators"]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"origins": forms.TextInput(), "origins": forms.TextInput(),
"collaborators": forms.CheckboxSelectMultiple(),
} }
labels = { labels = {
"origins": "Allowed Hostnames", "origins": "Allowed Hostnames",

View File

@ -10,13 +10,13 @@ def is_service_creator(user):
@rules.predicate @rules.predicate
def is_service_owner(service, user): def is_service_owner(user, service):
return service.owner == user return service.owner == user
@rules.predicate @rules.predicate
def is_service_collaborator(service, user): def is_service_collaborator(user, service):
return user in service.collaborators.all() return service.collaborators.filter(pk=user.pk).exists()
rules.add_perm("core.view_service", is_service_owner | is_service_collaborator) rules.add_perm("core.view_service", is_service_owner | is_service_collaborator)

View File

@ -6,7 +6,7 @@ This message can be safely ignored if you did not request a password reset. Clic
{{ password_reset_url }} {{ password_reset_url }}
{% endif %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you, {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
{{ site_name }} {{ site_name }}
{% endblocktrans %} {% endblocktrans %}
{% endautoescape %} {% endautoescape %}

View File

@ -13,7 +13,7 @@
<form method="POST" action="{{ action_url }}" class="max-w-lg"> <form method="POST" action="{{ action_url }}" class="max-w-lg">
{% csrf_token %} {% csrf_token %}
{{ form|a17t }} {{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans 'Change Password'}</button> <button type="submit" name="action" class="button ~urge !high">{% trans 'Change Password' %}</button>
</form> </form>
{% else %} {% else %}
<p>{% trans 'Your password is now changed.' %}</p> <p>{% trans 'Your password is now changed.' %}</p>

View File

@ -6,4 +6,5 @@
{% block card %} {% block card %}
<p>{% trans 'Your password is now changed.' %}</p> <p>{% trans 'Your password is now changed.' %}</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
{% endblock %} {% endblock %}

View File

@ -48,19 +48,23 @@
{% if can_create %} {% if can_create %}
{% url 'core:service_create' as url %} {% url 'core:service_create' as url %}
{% include 'core/includes/sidebar_portal.html' with label="+ Create" url=url %} {% include 'core/includes/sidebar_portal.html' with label="+ Create" url=url %}
<hr class="sep h-8">
{% endif %} {% endif %}
{% if user.collaborating_services.all %} {% if user.collaborating_services.all %}
<p class="ml-2 mt-8 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
{% for service in user.collaborating_services.all %} {% for service in user.collaborating_services.all %}
{% url 'core:service' service.uuid as url %} {% url 'core:service' service.uuid as url %}
{% include 'core/includes/sidebar_portal.html' with label=service.name url=url %} {% include 'core/includes/sidebar_portal.html' with label=service.name url=url %}
{% endfor %} {% endfor %}
<hr class="sep h-8">
{% endif %} {% endif %}
<p class="ml-2 mt-8 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p>
{% if user.is_authenticated %} {% if user.is_authenticated %}

View File

@ -0,0 +1,35 @@
{% load humanize helpers %}
<table class="table">
<thead>
<th>Session Start</th>
<th>Identity</th>
<th>Network</th>
<th class="rf">Duration</th>
<th class="rf">Hits</th>
</thead>
<tbody>
{% for session in object_list %}
<tr>
<td>
<a href="{% url 'core:service_session' object.pk session.pk %}" class="font-medium text-purple-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %}
<span class="badge ~positive">Online</span>
{% endif %}
</a>
</td>
<td>
{% if session.identifier %}
<span class="chip ~neutral">{{session.identifier}}</span>
{% else %}
<span class="text-gray-600">&mdash;</span>
{% endif %}
</td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td class="rf">{{session.duration|naturaldelta}}</td>
<td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -21,6 +21,6 @@
{% for object in services %} {% for object in services %}
{% include 'core/includes/service_overview.html' %} {% include 'core/includes/service_overview.html' %}
{% empty %} {% empty %}
<p>You don't have any services.</p> <p>You don't have any services on this Shynet instance yet.</p>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -178,6 +178,10 @@
</table> </table>
</div> </div>
</div> </div>
<a href="{% url 'core:service_session_list' service.uuid %}" class="button field w-auto">View individual sessions <div class="card ~neutral !low">
&rarr;</a> {% include 'core/includes/session_list.html' %}
<hr class="sep h-8">
<a href="{% url 'core:service_session_list' service.uuid %}" class="button ~neutral w-auto">View more sessions
&rarr;</a>
</div>
{% endblock %} {% endblock %}

View File

@ -11,39 +11,7 @@
{% block service_content %} {% block service_content %}
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll"> <div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll">
<table class="table"> {% include 'core/includes/session_list.html' %}
<thead>
<th>Time</th>
<th>Identity</th>
<th>Network</th>
<th class="rf">Duration</th>
<th class="rf">Hits</th>
</thead>
<tbody>
{% for session in object_list %}
<tr>
<td>
<a href="{% url 'core:service_session' object.pk session.pk %}" class="font-medium text-purple-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %}
<span class="badge ~positive">Online</span>
{% endif %}
</a>
</td>
<td>
{% if session.identifier %}
<span class="chip ~neutral">{{session.identifier}}</span>
{% else %}
<span class="text-gray-600">&mdash;</span>
{% endif %}
</td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td class="rf">{{session.duration|naturaldelta}}</td>
<td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% pagination page_obj request %} {% pagination page_obj request %}
{% endblock %} {% endblock %}

View File

@ -11,24 +11,29 @@
{% block service_content %} {% block service_content %}
<div class="max-w-xl content"> <div class="max-w-xl content">
<h5>Installation</h5> <h5>Installation</h5>
<p>Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.</p>
<div class="card ~neutral !high font-mono text-sm"> <div class="card ~neutral !high font-mono text-sm">
{% filter force_escape %}<noscript><img src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif"></noscript><script src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>{% endfilter %} {% filter force_escape %}<noscript><img
src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/pixel.gif"></noscript>
<script src="{{base_url}}/ingress/fc4008d3-f2fa-4500-9968-d96719e3819c/identifier/script.js"></script>
{% endfilter %}
</div> </div>
<hr class="sep h-4">
<h5>Settings</h5>
<form class="card ~neutral !low p-0" method="POST">
{% csrf_token %}
<div class="p-4">
{{form|a17t}}
</div>
<div class="section ~neutral !normal p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
</div>
<div>
<a href="{% url 'core:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
</div>
</div>
</form>
</div> </div>
<hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
{{form|a17t}}
</div>
<div class="section ~neutral !normal p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'core:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
</div>
<div>
<a href="{% url 'core:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
</div>
</div>
</form>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,16 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, reverse from django.shortcuts import get_object_or_404, reverse
from django.utils import timezone from django.utils import timezone
from django.views.generic import (CreateView, DeleteView, DetailView, ListView, from django.views.generic import (
TemplateView, UpdateView) CreateView,
DeleteView,
DetailView,
ListView,
TemplateView,
UpdateView,
)
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from django.db.models import Q
from analytics.models import Session from analytics.models import Session
@ -21,7 +28,9 @@ class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["services"] = Service.objects.filter(owner=self.request.user) data["services"] = Service.objects.filter(
Q(owner=self.request.user) | Q(collaborators__in=[self.request.user])
)
for service in data["services"]: for service in data["services"]:
service.stats = service.get_core_stats(data["start_date"], data["end_date"]) service.stats = service.get_core_stats(data["start_date"], data["end_date"])
return data return data
@ -51,6 +60,11 @@ class ServiceView(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"]) data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
data["object_list"] = Session.objects.filter(
service=self.get_object(),
start_time__lt=self.get_end_date(),
start_time__gt=self.get_start_date(),
).order_by("-start_time")[:10]
return data return data
@ -92,7 +106,7 @@ class ServiceSessionsListView(
service=self.get_object(), service=self.get_object(),
start_time__lt=self.get_end_date(), start_time__lt=self.get_end_date(),
start_time__gt=self.get_start_date(), start_time__gt=self.get_start_date(),
) ).order_by("-start_time")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)

View File

@ -26,9 +26,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = "q@@928+gjkhmcdpuwse0awn@#ygm#0etg11jlny+b*^cm5m-x!" SECRET_KEY = "q@@928+gjkhmcdpuwse0awn@#ygm#0etg11jlny+b*^cm5m-x!"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = os.getenv("DEBUG", "True") == "True"
ALLOWED_HOSTS = [] ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")
# Application definition # Application definition
@ -42,7 +42,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"django.contrib.humanize", "django.contrib.humanize",
"rules", "rules.apps.AutodiscoverRulesConfig",
"a17t", "a17t",
"core", "core",
"analytics", "analytics",
@ -168,6 +168,24 @@ MESSAGE_TAGS = {
messages.SUCCESS: "~positive", messages.SUCCESS: "~positive",
} }
# Email
SERVER_EMAIL = os.getenv("SERVER_EMAIL", "Shynet <noreply@shynet.example.com>")
DEFAULT_FROM_EMAIL = SERVER_EMAIL
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else:
EMAIL_HOST = os.environ.get("EMAIL_HOST")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 465))
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
EMAIL_USE_SSL = True
# Shynet # Shynet
ONLY_SUPERUSERS_CREATE = True # Can everyone create services, or only superusers? ONLY_SUPERUSERS_CREATE = True
# Can everyone create services, or only superusers?
# Note that in the current version of Shynet, being able to edit a service allows
# you to see every registered user on the Shynet instance. This will be changed in
# a future version.