Add core app

This commit is contained in:
Mark Veidemanis 2023-03-05 20:09:31 +00:00
parent b2bdc77496
commit a51797ef94
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
63 changed files with 1532 additions and 4 deletions

View File

@ -20,10 +20,7 @@ from django.contrib.auth.views import LogoutView
from django.urls import include, path from django.urls import include, path
from two_factor.urls import urlpatterns as tf_urls from two_factor.urls import urlpatterns as tf_urls
from core.views import ( from core.views import base, notifications
base,
notifications,
)
# from core.views.stripe_callbacks import Callback # from core.views.stripe_callbacks import Callback

14
core/__init__.py Normal file
View File

@ -0,0 +1,14 @@
import os
# import stripe
from django.conf import settings
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
# from redis import StrictRedis
# r = StrictRedis(unix_socket_path="/var/run/redis/redis.sock", db=0)
# if settings.STRIPE_TEST:
# stripe.api_key = settings.STRIPE_API_KEY_TEST
# else:
# stripe.api_key = settings.STRIPE_API_KEY_PROD

33
core/admin.py Normal file
View File

@ -0,0 +1,33 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm
from .models import NotificationSettings, User # AssetRestriction,; Plan,; Session,
# admin.site.__class__ = OTPAdminSite
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
# for model_cls, model_admin in admin.site._registry.items():
# otp_admin_site.register(model_cls, model_admin.__class__)
# Register your models here.
class CustomUserAdmin(UserAdmin):
# list_filter = ["plans"]
model = User
add_form = CustomUserCreationForm
fieldsets = (
*UserAdmin.fieldsets,
(
"Billing information",
{"fields": ("billing_provider_id", "payment_provider_id")},
),
)
class NotificationSettingsAdmin(admin.ModelAdmin):
list_display = ("user", "ntfy_topic", "ntfy_url")
admin.site.register(User, CustomUserAdmin)
admin.site.register(NotificationSettings, NotificationSettingsAdmin)

6
core/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"

50
core/forms.py Normal file
View File

@ -0,0 +1,50 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist
from django.forms import ModelForm
from mixins.restrictions import RestrictedFormMixin
from .models import NotificationSettings, User
# flake8: noqa: E501
class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = (
"username",
"email",
"first_name",
"last_name",
"password1",
"password2",
)
def save(self, commit=True):
user = super(NewUserForm, self).save(commit=False)
user.email = self.cleaned_data["email"]
if commit:
user.save()
return user
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = "__all__"
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = NotificationSettings
fields = (
"ntfy_topic",
"ntfy_url",
)
help_texts = {
"ntfy_topic": "The topic to send notifications to.",
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
}

0
core/lib/__init__.py Normal file
View File

32
core/lib/elastic.py Normal file
View File

@ -0,0 +1,32 @@
from datetime import datetime
from django.conf import settings
from elasticsearch import Elasticsearch
from core.util import logs
log = logs.get_logger(__name__)
client = None
def initialise_elasticsearch():
"""
Initialise the Elasticsearch client.
"""
auth = (settings.ELASTICSEARCH_USERNAME, settings.ELASTICSEARCH_PASSWORD)
client = Elasticsearch(
settings.ELASTICSEARCH_HOST, http_auth=auth, verify_certs=False
)
return client
def store_msg(index, msg):
global client
if not client:
client = initialise_elasticsearch()
if "ts" not in msg:
msg["ts"] = datetime.utcnow().isoformat()
result = client.index(index=index, body=msg)
if not result["result"] == "created":
log.error(f"Indexing of '{msg}' failed: {result}")

38
core/lib/notify.py Normal file
View File

@ -0,0 +1,38 @@
import requests
from core.util import logs
NTFY_URL = "https://ntfy.sh"
log = logs.get_logger(__name__)
# Actual function to send a message to a topic
def raw_sendmsg(msg, title=None, priority=None, tags=None, url=None, topic=None):
if url is None:
url = NTFY_URL
headers = {"Title": "Pluto"}
if title:
headers["Title"] = title
if priority:
headers["Priority"] = priority
if tags:
headers["Tags"] = tags
requests.post(
f"{url}/{topic}",
data=msg,
headers=headers,
)
# Sendmsg helper to send a message to a user's notification settings
def sendmsg(user, *args, **kwargs):
notification_settings = user.get_notification_settings()
if notification_settings.ntfy_topic is None:
# No topic set, so don't send
return
else:
topic = notification_settings.ntfy_topic
raw_sendmsg(*args, **kwargs, url=notification_settings.ntfy_url, topic=topic)

View File

View File

View File

@ -0,0 +1,33 @@
import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from django.core.management.base import BaseCommand
from core.util import logs
log = logs.get_logger("scheduling")
INTERVAL = 5
async def job():
print("Running schedule.")
class Command(BaseCommand):
def handle(self, *args, **options):
"""
Start the scheduling process.
"""
scheduler = AsyncIOScheduler()
log.debug(f"Scheduling checking process job every {INTERVAL} seconds")
scheduler.add_job(job, "interval", seconds=INTERVAL)
scheduler.start()
loop = asyncio.get_event_loop()
try:
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
log.info("Process terminating")
finally:
loop.close()

View File

@ -0,0 +1,59 @@
# Generated by Django 4.1.7 on 2023-03-04 14:11
import uuid
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('payment_provider_id', models.CharField(blank=True, max_length=255, null=True)),
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
('email', models.EmailField(max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='NotificationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

30
core/models.py Normal file
View File

@ -0,0 +1,30 @@
import uuid
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
# from core.lib.customers import get_or_create, update_customer_fields
from core.util import logs
log = logs.get_logger(__name__)
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
payment_provider_id = models.CharField(max_length=255, null=True, blank=True)
billing_provider_id = models.CharField(max_length=255, null=True, blank=True)
email = models.EmailField(unique=True)
def get_notification_settings(self):
return NotificationSettings.objects.get_or_create(user=self)[0]
class NotificationSettings(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return f"Notification settings for {self.user}"

2
core/static/css/bulma-tooltip.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
core/static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
core/static/css/gridstack.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
{
const data = document.currentScript.dataset;
const isDebug = data.debug === "True";
if (isDebug) {
document.addEventListener("htmx:beforeOnLoad", function (event) {
const xhr = event.detail.xhr;
if (xhr.status == 500 || xhr.status == 404) {
// Tell htmx to stop processing this response
event.stopPropagation();
document.children[0].innerHTML = xhr.response;
// Run Djangos inline script
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
(1, eval)(document.scripts[0].innerText);
// Need to directly call Djangos onload function since browser wont
window.onload();
}
});
}
}

File diff suppressed because one or more lines are too long

16
core/static/js/gridstack.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
core/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
core/static/js/hyperscript.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
core/static/js/magnet.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,27 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}
}
});
})();

View File

@ -0,0 +1,15 @@
{
"background_color": "white",
"description": "Cryptocurrency arbitrage automation",
"display": "fullscreen",
"icons": [
{
"src": "/static/logo.png",
"sizes": "800x800",
"type": "image/png"
}
],
"name": "Pluto Arbitrage",
"short_name": "Pluto",
"start_url": "/"
}

44
core/static/modal.js Normal file
View File

@ -0,0 +1,44 @@
// var modal = document.querySelector('.modal'); // assuming you have only 1
var modal = document.getElementById("modal");
var html = document.querySelector('html');
var disableModal = function() {
modal.classList.remove('is-active');
html.classList.remove('is-clipped');
var modal_refresh = document.getElementsByClassName("modal-refresh");
for(var i = 0; i < modal_refresh.length; i++) {
modal_refresh[i].remove();
}
}
var elements = document.querySelectorAll('.modal-background');
for(var i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', function(e) {
// elements[i].preventDefault();
disableModal();
});
}
var elements = document.querySelectorAll('.modal-close');
for(var i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', function(e) {
// elements[i].preventDefault();
disableModal();
});
}
function activateButtons() {
var elements = document.querySelectorAll('.modal-close-button');
for(var i = 0; i < elements.length; i++) {
elements[i].addEventListener('click', function(e) {
// elements[i].preventDefault();
disableModal();
});
}
}
activateButtons();
// modal.querySelector('.modal-close-button').addEventListener('click', function(e) {
// e.preventDefault();
// modal.classList.remove('is-active');
// html.classList.remove('is-clipped');
// });

317
core/templates/base.html Normal file
View File

@ -0,0 +1,317 @@
{% load static %}
{% load cache %}
<!DOCTYPE html>
<html lang="en-GB">
{# cache 600 head request.path_info #}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pluto - {{ request.path_info }}</title>
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
<script src="{% static 'js/gridstack-all.js' %}"></script>
<script defer src="{% static 'js/magnet.min.js' %}"></script>
<script>
document.addEventListener("restore-scroll", function(event) {
var scrollpos = localStorage.getItem('scrollpos');
if (scrollpos) {
window.scrollTo(0, scrollpos)
};
});
document.addEventListener("htmx:beforeSwap", function(event) {
localStorage.setItem('scrollpos', window.scrollY);
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
<style>
.icon { border-bottom: 0px !important;}
.wrap {
word-wrap: break-word;
}
.nowrap-parent {
white-space: nowrap;
}
.nowrap-child {
display: inline-block;
}
.htmx-indicator{
opacity:0;
transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
.htmx-request.htmx-indicator{
opacity:1
}
.tooltiptext {
visibility: hidden;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
.rounded-tooltip:hover .tooltiptext {
visibility: visible;
}
.table {
background: transparent !important;
}
tr {
transition: all 0.2s ease-in-out;
}
tr:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
a.panel-block {
transition: all 0.2s ease-in-out;
}
a.panel-block:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
.panel, .box, .modal {
background-color:rgba(250, 250, 250, 0.5) !important;
}
.modal, .modal.box{
background-color:rgba(210, 210, 210, 0.9) !important;
}
.modal-background{
background-color:rgba(255, 255, 255, 0.3) !important;
}
.has-background-grey-lighter{
background-color:rgba(219, 219, 219, 0.5) !important;
}
.navbar {
background-color:rgba(0, 0, 0, 0.03) !important;
}
.grid-stack-item-content {
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
}
.panel {
display: flex !important;
flex-direction: column !important;
overflow: hidden;
}
.panel-block {
overflow-y:auto;
overflow-x:auto;
min-height: 90%;
display: block;
}
.floating-window {
/* background-color:rgba(210, 210, 210, 0.6) !important; */
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
max-height: 300px;
z-index: 9000;
position: absolute;
top: 50px;
left: 50px;
}
.floating-window .panel {
background-color:rgba(250, 250, 250, 0.8) !important;
}
.float-right {
float: right;
padding-right: 5px;
padding-left: 5px;
}
.grid-stack-item:hover .ui-resizable-handle {
display: block !important;
}
.ui-resizable-handle {
z-index: 39 !important;
}
</style>
<!-- Piwik --> {# Yes it's in the source, fight me #}
<script type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
_paq.push(['setTrackerUrl', 'https://api-dd242151ac50129c3320f209578a406c.s.zm.is']);
_paq.push(['setSiteId', 6]);
_paq.push(['setApiToken', 'owVUM8fMHxHtyDoIFdyZxx1TWTNECV5ImmoKI1y5muc']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Piwik Code -->
</head>
{# endcache #}
<body>
{# cache 600 nav request.user.id #}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'home' %}">
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="bar" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'home' %}">
Home
</a>
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Account
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
Security
</a>
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
Notifications
</a>
</div>
</div>
{% endif %}
{% if settings.BILLING_ENABLED %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{# url 'billing' #}">
Billing
</a>
{% endif %}
{% endif %}
<a class="navbar-item add-button">
Install
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
{% if not user.is_authenticated %}
<a class="button" href="{% url 'signup' %}">
<strong>Sign up</strong>
</a>
<a class="button" href="{% url 'two_factor:login' %}">
Log in
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="button" href="{% url 'logout' %}">Logout</a>
{% endif %}
</div>
</div>
</div>
</div>
</nav>
{# endcache #}
<script>
let deferredPrompt;
const addBtn = document.querySelector('.add-button');
addBtn.style.display = 'none';
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
// Update UI to notify the user they can add to home screen
addBtn.style.display = 'block';
addBtn.addEventListener('click', (e) => {
// hide our user interface that shows our A2HS button
addBtn.style.display = 'none';
// Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
});
</script>
{% block outer_content %}
{% endblock %}
<section class="section">
<div class="container">
{% block content_wrapper %}
{% block content %}
{% endblock %}
{% endblock %}
<div id="modals-here">
</div>
<div id="windows-here">
</div>
<div id="widgets-here" style="display: none;">
</div>
</div>
</section>
</body>
</html>

103
core/templates/index.html Normal file
View File

@ -0,0 +1,103 @@
{% extends "base.html" %}
{% load static %}
{% load joinsep %}
{% block content %}
<div class="grid-stack" id="grid-stack-main">
<script>
var grid = GridStack.init({
cellHeight: 20,
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
let containers = htmx.findAll('#widget');
for (let x = 0, len = containers.length; x < len; x++) {
container = containers[x];
// get the scripts, they won't be run on the new element so we need to eval them
let widgetelement = container.firstElementChild.cloneNode(true);
console.log(widgetelement);
var scripts = htmx.findAll(widgetelement, "script");
var new_id = widgetelement.id;
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, "#"+new_id);
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
}
}
}
// clear the queue element
container.outerHTML = "";
// container.firstElementChild.outerHTML = "";
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
// update the size of the widget according to its content
var added_widget = htmx.find(grid_element, "#"+new_id);
var itemContent = htmx.find(added_widget, ".control");
var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0;
var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = {
h: height,
}
grid.update(
added_widget,
opts
);
// run the JS scripts inside the added element again
for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML);
}
}
// clear the containers we just added
// for (let x = 0, len = containers.length; x < len; x++) {
// container = containers[x];
// container.inner = "";
// }
grid.compact();
});
</script>
<!-- <div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'example' type='widget' #}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'example' type='widget' #}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'example' type='widget' #}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
</div> -->
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<form method="POST" class="box">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<button class="button">
Login
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<div class="box">
<p class="has-text-danger">Registration closed.</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<form method="POST" class="box">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<button class="button">
Sign up
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1 @@
{% extends 'base.html' %}

View File

@ -0,0 +1,16 @@
{% extends "two_factor/_base.html" %}
{% block content_wrapper %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column box is-5-tablet is-5-desktop is-4-widescreen">
{% block content %}{% endblock content %}
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% load i18n %}
<div class="buttons">
{% if cancel_url %}
<a href="{{ cancel_url }}"
class="button">{% trans "Cancel" %}</a>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit"
value="{{ wizard.steps.prev }}"
class="button">{% trans "Back" %}</button>
{% else %}
<button disabled name="" type="button" class="button">{% trans "Back" %}</button>
{% endif %}
<button type="submit" class="button">{% trans "Next" %}</button>
</div>

View File

@ -0,0 +1,6 @@
{% load crispy_forms_tags %}
<table class="is-3">
{{ wizard.management_form|crispy }}
{{ wizard.form|crispy }}
</table>

View File

@ -0,0 +1,28 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}Backup tokens can be used when your primary and backup
phone numbers aren't available. The backup tokens below can be used
for login verification. If you've used up all your backup tokens, you
can generate a new set of backup tokens. Only the backup tokens shown
below will be valid.{% endblocktrans %}</p>
{% if device.token_set.count %}
<ul>
{% for token in device.token_set.all %}
<li>{{ token.token }}</li>
{% endfor %}
</ul>
<p class="subtitle">{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
{% else %}
<p class="subtitle">{% trans "You don't have any backup codes yet." %}</p>
{% endif %}
<form method="post">{% csrf_token %}{{ form }}
<a href="{% url 'two_factor:profile'%}"
class="float-right button">{% trans "Back to Account Security" %}</a>
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Login" %}{% endblock %}</h1>
{% if wizard.steps.current == 'auth' %}
<p class="subtitle">{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'token' %}
{% if device.method == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% else %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the tokens generated by your token
generator.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'backup' %}
<p class="subtitle">{% blocktrans trimmed %}Use this form for entering backup tokens for logging in.
These tokens have been generated for you to print and keep safe. Please
enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% if other_devices %}
<p class="subtitle">{% trans "Or, alternatively, use one of your backup phones:" %}</p>
<p class="subtitle">
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}"
class="button" type="submit">
{{ other.generate_challenge_button_title }}
</button>
{% endfor %}</p>
{% endif %}
{% if backup_tokens %}
<p class="subtitle">{% trans "As a last resort, you can use a backup token:" %}</p>
<p class="subtitle">
<button name="wizard_goto_step" type="submit" value="backup"
class="button">{% trans "Use Backup Token" %}</button>
</p>
{% endif %}
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}The page you requested, enforces users to verify using
two-factor authentication for security reasons. You need to enable these
security features in order to access this page.{% endblocktrans %}</p>
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<div class="buttons">
<a href="javascript:history.go(-1)"
class="float-right button">{% trans "Go back" %}</a>
<a href="{% url 'two_factor:setup' %}" class="button">
{% trans "Enable Two-Factor Authentication" %}</a>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
{% if wizard.steps.current == 'setup' %}
<p class="subtitle">{% blocktrans trimmed %}You'll be adding a backup phone number to your
account. This number will be used if your primary method of
registration is not available.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
<p class="subtitle">{% blocktrans trimmed %}We've sent a token to your phone number. Please
enter the token you've received.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
{% if wizard.steps.current == 'welcome' %}
<p class="subtitle">{% blocktrans trimmed %}You are about to take your account security to the
next level. Follow the steps in this wizard to enable two-factor
authentication.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'method' %}
<p class="subtitle">{% blocktrans trimmed %}Please select which authentication method you would
like to use.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'generator' %}
<p class="subtitle">{% blocktrans trimmed %}To start using a token generator, please use your
smartphone to scan the QR code below. For example, use Google
Authenticator. Then, enter the token generated by the app.
{% endblocktrans %}</p>
<p class="subtitle"><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
{% elif wizard.steps.current == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to receive the
text messages on. This number will be validated in the next step.
{% endblocktrans %}</p>
{% elif wizard.steps.current == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
This number will be validated in the next step. {% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
{% if challenge_succeeded %}
{% if device.method == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% endif %}
{% else %}
<p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've
encountered an issue with the selected authentication method. Please
go back and verify that you entered your information correctly, try
again, or use a different authentication method instead. If the issue
persists, contact the site administrator.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'yubikey' %}
<p class="subtitle">{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
token in the field below. Your YubiKey will be linked to your
account.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
authentication.{% endblocktrans %}</p>
{% if not phone_methods %}
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
class="button">{% trans "Back to Account Security" %}</a></p>
{% else %}
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
your primary token device. To enable account recovery, add a phone
number.{% endblocktrans %}</p>
<a href="{% url 'two_factor:profile' %}"
class="float-right button">{% trans "Back to Account Security" %}</a>
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
class="button">{% trans "Add Phone Number" %}</a></p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}You are about to disable two-factor authentication. This
weakens your account security, are you sure?{% endblocktrans %}</p>
<form method="post">
{% csrf_token %}
<table>{{ form }}</table>
<button class="button"
type="submit">{% trans "Disable" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends "two_factor/_base.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
{% if default_device %}
{% if default_device_type == 'TOTPDevice' %}
<p class="subtitle">{% trans "Tokens will be generated by your token generator." %}</p>
{% elif default_device_type == 'PhoneDevice' %}
<p class="subtitle">{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}</p>
{% elif default_device_type == 'RemoteYubikeyDevice' %}
<p class="subtitle">{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
{% endif %}
{% if available_phone_methods %}
<h2 class="title is-4">{% trans "Backup Phone Numbers" %}</h2>
<p class="subtitle">{% blocktrans trimmed %}If your primary method is not available, we are able to
send backup tokens to the phone numbers listed below.{% endblocktrans %}</p>
<ul>
{% for phone in backup_phones %}
<li>
{{ phone.generate_challenge_button_title }}
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
onsubmit="return confirm({% trans 'Are you sure?' %})">
{% csrf_token %}
<button class="button is-warning"
type="submit">{% trans "Unregister" %}</button>
</form>
</li>
{% endfor %}
</ul>
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
class="button">{% trans "Add Phone Number" %}</a></p>
{% endif %}
<h2 class="title is-4">{% trans "Backup Tokens" %}</h2>
<p class="subtitle">
{% blocktrans trimmed %}If you don't have any device with you, you can access
your account using backup tokens.{% endblocktrans %}
{% blocktrans trimmed count counter=backup_tokens %}
You have only one backup token remaining.
{% plural %}
You have {{ counter }} backup tokens remaining.
{% endblocktrans %}
</p>
<p class="subtitle"><a href="{% url 'two_factor:backup_tokens' %}"
class="button">{% trans "Show Codes" %}</a></p>
<h3 class="title is-5">{% trans "Disable Two-Factor Authentication" %}</h3>
<p class="subtitle">{% blocktrans trimmed %}However we strongly discourage you to do so, you can
also disable two-factor authentication for your account.{% endblocktrans %}</p>
<p class="subtitle"><a class="button" href="{% url 'two_factor:disable' %}">
{% trans "Disable Two-Factor Authentication" %}</a></p>
{% else %}
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<p class="subtitle"><a href="{% url 'two_factor:setup' %}" class="button">
{% trans "Enable Two-Factor Authentication" %}</a>
</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<Gather timeout="15" numDigits="1" finishOnKey="">
<Say language="{{ locale }}">{% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %}</Say>
</Gather>
<Say language="{{ locale }}">{% trans "You didnt press any keys. Good bye." %}</Say>
</Response>

View File

@ -0,0 +1,5 @@
{% load i18n %}
{% blocktrans trimmed %}
Your OTP token is {{ token }}
{% endblocktrans %}

View File

@ -0,0 +1,12 @@
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<Say language="{{ locale }}">{% trans "Your token is:" %}</Say>
<Pause>
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
<Pause>
{% endfor %} <Say language="{{ locale }}">{% trans "Repeat:" %}</Say>
<Pause>
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
<Pause>
{% endfor %} <Say language="{{ locale }}">{% trans "Good bye." %}</Say>
</Response>

View File

View File

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def index(h, key):
return h[key]

View File

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def joinsep(lst, sep):
return sep.join(lst)

View File

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def nsep(lst):
return "\n".join(lst)

View File

@ -0,0 +1,9 @@
import orjson
from django import template
register = template.Library()
@register.filter
def pretty(data):
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8")

View File

@ -0,0 +1,10 @@
import urllib.parse
from django import template
register = template.Library()
@register.filter
def urlsafe(h):
return urllib.parse.quote(h, safe="")

0
core/tests/__init__.py Normal file
View File

0
core/util/__init__.py Normal file
View File

View File

@ -0,0 +1,69 @@
"""
Export Django settings to templates
https://github.com/jakubroztocil/django-settings-export
"""
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
__version__ = "1.2.1"
VARIABLE_NAME = getattr(django_settings, "SETTINGS_EXPORT_VARIABLE_NAME", "settings")
class SettingsExportError(ImproperlyConfigured):
"""Base error indicating misconfiguration."""
class UndefinedSettingError(SettingsExportError):
"""An undefined setting name included in SETTINGS_EXPORT."""
class UnexportedSettingError(SettingsExportError):
"""An unexported setting has been accessed from a template."""
def settings_export(request):
"""
The template context processor that adds settings defined in
`SETTINGS_EXPORT` to the context. If SETTINGS_EXPORT_VARIABLE_NAME is not
set, the context variable will be `settings`.
"""
variable_name = getattr(
django_settings, "SETTINGS_EXPORT_VARIABLE_NAME", "settings"
)
return {variable_name: _get_exported_settings()}
class ExportedSettings(dict):
def __getitem__(self, item):
"""Fail loudly if accessing a setting that is not exported."""
try:
return super(ExportedSettings, self).__getitem__(item)
except KeyError:
if hasattr(self, item):
# Let the KeyError propagate so that Django templates
# can access the existing attribute (e.g. `items()`).
raise
raise UnexportedSettingError(
"The `{key}` setting key is not accessible"
' from templates: add "{key}" to'
" `settings.SETTINGS_EXPORT` to change that.".format(key=item)
)
def _get_exported_settings():
exported_settings = ExportedSettings()
for key in getattr(django_settings, "SETTINGS_EXPORT", []):
try:
value = getattr(django_settings, key)
except AttributeError:
raise UndefinedSettingError(
'"settings.%s" is included in settings.SETTINGS_EXPORT '
"but it does not exist. " % key
)
exported_settings[key] = value
return exported_settings

68
core/util/logs.py Normal file
View File

@ -0,0 +1,68 @@
# Other library imports
import logging
log = logging.getLogger("util")
debug = True
# Color definitions
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
COLORS = {
"WARNING": YELLOW,
"INFO": WHITE,
"DEBUG": BLUE,
"CRITICAL": YELLOW,
"ERROR": RED,
}
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
def formatter_message(message, use_color=True):
if use_color:
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
else:
message = message.replace("$RESET", "").replace("$BOLD", "")
return message
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
levelname_color = (
COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
)
record.levelname = levelname_color
return logging.Formatter.format(self, record)
def get_logger(name):
# Define the logging format
FORMAT = "%(asctime)s %(levelname)18s $BOLD%(name)13s$RESET - %(message)s"
COLOR_FORMAT = formatter_message(FORMAT, True)
color_formatter = ColoredFormatter(COLOR_FORMAT)
# formatter = logging.Formatter(
# Why is this so complicated?
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# ch.setFormatter(formatter)
ch.setFormatter(color_formatter)
# Define the logger on the base class
log = logging.getLogger(name)
log.setLevel(logging.INFO)
if debug:
log.setLevel(logging.DEBUG)
ch.setLevel(logging.DEBUG)
# Add the handler and stop it being silly and printing everything twice
log.addHandler(ch)
log.propagate = False
return log

0
core/views.py Normal file
View File

0
core/views/__init__.py Normal file
View File

44
core/views/base.py Normal file
View File

@ -0,0 +1,44 @@
import logging
# import stripe
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy
from django.views import View
from django.views.generic.edit import CreateView
from core.forms import NewUserForm
from core.lib.notify import raw_sendmsg
logger = logging.getLogger(__name__)
# Create your views here
class Home(LoginRequiredMixin, View):
template_name = "index.html"
def get(self, request):
return render(request, self.template_name)
class Signup(CreateView):
form_class = NewUserForm
success_url = reverse_lazy("two_factor:login")
template_name = "registration/signup.html"
def form_valid(self, form):
"""If the form is valid, save the associated model."""
self.object = form.save()
raw_sendmsg(
f"New user signup: {self.object.username} - {self.object.email}",
title="New user",
topic=settings.NOTIFY_TOPIC,
)
return super().form_valid(form)
def get(self, request, *args, **kwargs):
if not settings.REGISTRATION_OPEN:
return render(request, "registration/registration_closed.html")
return super().get(request, *args, **kwargs)

View File

@ -0,0 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
class SuperUserRequiredMixin(LoginRequiredMixin, UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser

View File

@ -0,0 +1,30 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectUpdate
from core.forms import NotificationSettingsForm
from core.models import NotificationSettings
# Notifications - we create a new notification settings object if there isn't one
# Hence, there is only an update view, not a create view.
class NotificationsUpdate(LoginRequiredMixin, ObjectUpdate):
model = NotificationSettings
form_class = NotificationSettingsForm
page_title = "Update your notification settings"
page_subtitle = (
"At least the topic must be set if you want to receive notifications."
)
submit_url_name = "notifications_update"
submit_url_args = ["type"]
pk_required = False
hide_cancel = True
def get_object(self, **kwargs):
notification_settings, _ = NotificationSettings.objects.get_or_create(
user=self.request.user
)
return notification_settings