commit a942c094b32bbe1c047d5e02d2346987d9c0026f Author: Mark Veidemanis Date: Fri Feb 7 20:57:26 2025 +0000 Add basic project files and templates diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60c2f6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +# lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +stack.env +.venv +env/ +venv/ +env-glibc/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +.bash_history +.vscode/ +core/static/admin +core/static/debug_toolbar +signal-cli-config/ +docker/data/ +static/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cd4b09 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Envelope +Template Django app. + +## Setting up the environment +Create the virtual environment, enable it, and install the dependencies. +```shell +$ python3 -m venv env +$ source env/bin/activate +(env) $ pip install -r docker/prod/requirements.prod.txt +``` + +## Local settings +You'll need to copy the `app/local_settings.example.py` file to `app/local_settings.py`. The project won't start otherwise. +``` +$ cp app/local_settings.example.py app/local_settings.py +``` + +## stack.env +The stack.env file referenced is a Portainer special. This is where Portainer would put a file containing all the environment variables set up in its UI. +To run it manually, you will need to copy `stack.env.example` to `stack.env` in the project root. + +## Running database migrations +Now we need to run the database migrations in order to get a working database. +```shell +(env) $ python manage.py migrate +``` +Note that these are automatically run by a step in the compose file in production. +You won't need to do that manually. + +## Creating a superuser +In order to access Django admin, we need a superuser. +```shell +(env) $ python manage.py createsuperuser +Username: t2 +Email address: t2@google.com +Password: +Password (again): +Superuser created successfully. +``` + +## Running +The Docker Compose file is located in `docker/docker-compose.prod.yml`. +There is a shortcut to run it: `make run`. + +## Stopping +To stop the containers, run `make stop`. + +## Setup +This setup may be different from what you've seen before. + +### Uvicorn +There is a Uvicorn worker in the `app` container listening on `/var/run/socks/app.sock`. This is the bit that runs the actual code. + +### Nginx +Nginx runs in the `nginx` container and proxies requests to Uvicorn thanks to a mounted and shared directory. No TCP required. + +### Pre-start steps +There's a few commands running before start to ensure Django works correctly. + +#### Migration +The `migration` container step runs the migrations so you don't need to remember to do it. + +#### Collectstatic +The `collectstatic` container step collects all static files from plugins and puts them in the `core/static` folder. This folder is served straight from Nginx without going through Uvicorn. diff --git a/core/lib/notify.py b/core/lib/notify.py new file mode 100644 index 0000000..258d6e5 --- /dev/null +++ b/core/lib/notify.py @@ -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": "GIA"} + 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) diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..6cc72a3 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+
+
+
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/logout.html b/core/templates/registration/logout.html new file mode 100644 index 0000000..e69de29 diff --git a/core/templates/registration/registration_closed.html b/core/templates/registration/registration_closed.html new file mode 100644 index 0000000..f0a7ca1 --- /dev/null +++ b/core/templates/registration/registration_closed.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+
+
+
+

Registration closed.

+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/registration/signup.html b/core/templates/registration/signup.html new file mode 100644 index 0000000..7beef31 --- /dev/null +++ b/core/templates/registration/signup.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block content %} +
+
+
+
+
+
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/two_factor/_base.html b/core/templates/two_factor/_base.html new file mode 100644 index 0000000..838aa18 --- /dev/null +++ b/core/templates/two_factor/_base.html @@ -0,0 +1 @@ +{% extends 'base.html' %} \ No newline at end of file diff --git a/core/templates/two_factor/_base_focus.html b/core/templates/two_factor/_base_focus.html new file mode 100644 index 0000000..104ce14 --- /dev/null +++ b/core/templates/two_factor/_base_focus.html @@ -0,0 +1,16 @@ +{% extends "two_factor/_base.html" %} + +{% block content_wrapper %} +
+
+
+
+
+ {% block content %}{% endblock content %} +
+
+
+
+
+{% endblock %} + diff --git a/core/templates/two_factor/_wizard_actions.html b/core/templates/two_factor/_wizard_actions.html new file mode 100644 index 0000000..74cbe3d --- /dev/null +++ b/core/templates/two_factor/_wizard_actions.html @@ -0,0 +1,16 @@ +{% load i18n %} + +
+ {% if cancel_url %} + {% trans "Cancel" %} + {% endif %} + {% if wizard.steps.prev %} + + {% else %} + + {% endif %} + +
\ No newline at end of file diff --git a/core/templates/two_factor/_wizard_forms.html b/core/templates/two_factor/_wizard_forms.html new file mode 100644 index 0000000..7a9eb9e --- /dev/null +++ b/core/templates/two_factor/_wizard_forms.html @@ -0,0 +1,6 @@ +{% load crispy_forms_tags %} + + + {{ wizard.management_form|crispy }} + {{ wizard.form|crispy }} +
diff --git a/core/templates/two_factor/core/backup_tokens.html b/core/templates/two_factor/core/backup_tokens.html new file mode 100644 index 0000000..ca4f9ff --- /dev/null +++ b/core/templates/two_factor/core/backup_tokens.html @@ -0,0 +1,28 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Backup Tokens" %}{% endblock %}

+

{% 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 %}

+ + {% if device.token_set.count %} + +

{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}

+ {% else %} +

{% trans "You don't have any backup codes yet." %}

+ {% endif %} + +
{% csrf_token %}{{ form }} + {% trans "Back to Account Security" %} + +
+{% endblock %} diff --git a/core/templates/two_factor/core/login.html b/core/templates/two_factor/core/login.html new file mode 100644 index 0000000..c36c03d --- /dev/null +++ b/core/templates/two_factor/core/login.html @@ -0,0 +1,52 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Login" %}{% endblock %}

+ + {% if wizard.steps.current == 'auth' %} +

{% blocktrans %}Enter your credentials.{% endblocktrans %}

+ {% elif wizard.steps.current == 'token' %} + {% if device.method == 'call' %} +

{% blocktrans trimmed %}We are calling your phone right now, please enter the + digits you hear.{% endblocktrans %}

+ {% elif device.method == 'sms' %} +

{% blocktrans trimmed %}We sent you a text message, please enter the tokens we + sent.{% endblocktrans %}

+ {% else %} +

{% blocktrans trimmed %}Please enter the tokens generated by your token + generator.{% endblocktrans %}

+ {% endif %} + {% elif wizard.steps.current == 'backup' %} +

{% 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 %}

+ {% endif %} + +
{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + + + {% if other_devices %} +

{% trans "Or, alternatively, use one of your backup phones:" %}

+

+ {% for other in other_devices %} + + {% endfor %}

+ {% endif %} + {% if backup_tokens %} +

{% trans "As a last resort, you can use a backup token:" %}

+

+ +

+ {% endif %} + + {% include "two_factor/_wizard_actions.html" %} +
+{% endblock %} diff --git a/core/templates/two_factor/core/otp_required.html b/core/templates/two_factor/core/otp_required.html new file mode 100644 index 0000000..e5b9e5b --- /dev/null +++ b/core/templates/two_factor/core/otp_required.html @@ -0,0 +1,22 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Permission Denied" %}{% endblock %}

+ +

{% 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 %}

+ +

{% blocktrans trimmed %}Two-factor authentication is not enabled for your + account. Enable two-factor authentication for enhanced account + security.{% endblocktrans %}

+
+ + {% trans "Go back" %} + + {% trans "Enable Two-Factor Authentication" %} +
+ +{% endblock %} diff --git a/core/templates/two_factor/core/phone_register.html b/core/templates/two_factor/core/phone_register.html new file mode 100644 index 0000000..11182ff --- /dev/null +++ b/core/templates/two_factor/core/phone_register.html @@ -0,0 +1,24 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Add Backup Phone" %}{% endblock %}

+ + {% if wizard.steps.current == 'setup' %} +

{% 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 %}

+ {% elif wizard.steps.current == 'validation' %} +

{% blocktrans trimmed %}We've sent a token to your phone number. Please + enter the token you've received.{% endblocktrans %}

+ {% endif %} + +
{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + + + {% include "two_factor/_wizard_actions.html" %} +
+{% endblock %} diff --git a/core/templates/two_factor/core/setup.html b/core/templates/two_factor/core/setup.html new file mode 100644 index 0000000..bd5c93d --- /dev/null +++ b/core/templates/two_factor/core/setup.html @@ -0,0 +1,56 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}

+ {% if wizard.steps.current == 'welcome' %} +

{% 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 %}

+ {% elif wizard.steps.current == 'method' %} +

{% blocktrans trimmed %}Please select which authentication method you would + like to use.{% endblocktrans %}

+ {% elif wizard.steps.current == 'generator' %} +

{% 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 %}

+

QR Code

+ {% elif wizard.steps.current == 'sms' %} +

{% 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 %}

+ {% elif wizard.steps.current == 'call' %} +

{% blocktrans trimmed %}Please enter the phone number you wish to be called on. + This number will be validated in the next step. {% endblocktrans %}

+ {% elif wizard.steps.current == 'validation' %} + {% if challenge_succeeded %} + {% if device.method == 'call' %} +

{% blocktrans trimmed %}We are calling your phone right now, please enter the + digits you hear.{% endblocktrans %}

+ {% elif device.method == 'sms' %} +

{% blocktrans trimmed %}We sent you a text message, please enter the tokens we + sent.{% endblocktrans %}

+ {% endif %} + {% else %} + + {% endif %} + {% elif wizard.steps.current == 'yubikey' %} +

{% 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 %}

+ {% endif %} + +
{% csrf_token %} + {% include "two_factor/_wizard_forms.html" %} + + {# hidden submit button to enable [enter] key #} + + + {% include "two_factor/_wizard_actions.html" %} +
+{% endblock %} diff --git a/core/templates/two_factor/core/setup_complete.html b/core/templates/two_factor/core/setup_complete.html new file mode 100644 index 0000000..0d947ab --- /dev/null +++ b/core/templates/two_factor/core/setup_complete.html @@ -0,0 +1,24 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}

+ +

{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor + authentication.{% endblocktrans %}

+ + {% if not phone_methods %} +

{% trans "Back to Account Security" %}

+ {% else %} +

{% 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 %}

+ + {% trans "Back to Account Security" %} +

{% trans "Add Phone Number" %}

+ {% endif %} + +{% endblock %} diff --git a/core/templates/two_factor/profile/disable.html b/core/templates/two_factor/profile/disable.html new file mode 100644 index 0000000..2db864a --- /dev/null +++ b/core/templates/two_factor/profile/disable.html @@ -0,0 +1,14 @@ +{% extends "two_factor/_base_focus.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}

+

{% blocktrans trimmed %}You are about to disable two-factor authentication. This + weakens your account security, are you sure?{% endblocktrans %}

+
+ {% csrf_token %} + {{ form }}
+ +
+{% endblock %} diff --git a/core/templates/two_factor/profile/profile.html b/core/templates/two_factor/profile/profile.html new file mode 100644 index 0000000..f72e7bc --- /dev/null +++ b/core/templates/two_factor/profile/profile.html @@ -0,0 +1,63 @@ +{% extends "two_factor/_base.html" %} +{% load i18n %} + +{% block content %} +

{% block title %}{% trans "Account Security" %}{% endblock %}

+ + {% if default_device %} + {% if default_device_type == 'TOTPDevice' %} +

{% trans "Tokens will be generated by your token generator." %}

+ {% elif default_device_type == 'PhoneDevice' %} +

{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}

+ {% elif default_device_type == 'RemoteYubikeyDevice' %} +

{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}

+ {% endif %} + + {% if available_phone_methods %} +

{% trans "Backup Phone Numbers" %}

+

{% blocktrans trimmed %}If your primary method is not available, we are able to + send backup tokens to the phone numbers listed below.{% endblocktrans %}

+ +

{% trans "Add Phone Number" %}

+ {% endif %} + +

{% trans "Backup Tokens" %}

+

+ {% 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 %} +

+

{% trans "Show Codes" %}

+ +

{% trans "Disable Two-Factor Authentication" %}

+

{% blocktrans trimmed %}However we strongly discourage you to do so, you can + also disable two-factor authentication for your account.{% endblocktrans %}

+

+ {% trans "Disable Two-Factor Authentication" %}

+ {% else %} +

{% blocktrans trimmed %}Two-factor authentication is not enabled for your + account. Enable two-factor authentication for enhanced account + security.{% endblocktrans %}

+

+ {% trans "Enable Two-Factor Authentication" %} +

+ {% endif %} +{% endblock %} diff --git a/core/templates/two_factor/twilio/press_a_key.xml b/core/templates/two_factor/twilio/press_a_key.xml new file mode 100644 index 0000000..85a3619 --- /dev/null +++ b/core/templates/two_factor/twilio/press_a_key.xml @@ -0,0 +1,7 @@ +{% load i18n %} + + + {% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %} + + {% trans "You didn’t press any keys. Good bye." %} + diff --git a/core/templates/two_factor/twilio/sms_message.html b/core/templates/two_factor/twilio/sms_message.html new file mode 100644 index 0000000..b680488 --- /dev/null +++ b/core/templates/two_factor/twilio/sms_message.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% blocktrans trimmed %} + Your OTP token is {{ token }} +{% endblocktrans %} + diff --git a/core/templates/two_factor/twilio/token.xml b/core/templates/two_factor/twilio/token.xml new file mode 100644 index 0000000..4eafffe --- /dev/null +++ b/core/templates/two_factor/twilio/token.xml @@ -0,0 +1,12 @@ +{% load i18n %} + + {% trans "Your token is:" %} + +{% for digit in token %} {{ digit }} + +{% endfor %} {% trans "Repeat:" %} + +{% for digit in token %} {{ digit }} + +{% endfor %} {% trans "Good bye." %} + diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/index.py b/core/templatetags/index.py new file mode 100644 index 0000000..aa3e4c8 --- /dev/null +++ b/core/templatetags/index.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter +def index(h, key): + return h[key] diff --git a/core/templatetags/joinsep.py b/core/templatetags/joinsep.py new file mode 100644 index 0000000..4f0c06b --- /dev/null +++ b/core/templatetags/joinsep.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter +def joinsep(lst, sep): + return sep.join(lst) diff --git a/core/templatetags/nsep.py b/core/templatetags/nsep.py new file mode 100644 index 0000000..742ae57 --- /dev/null +++ b/core/templatetags/nsep.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.filter +def nsep(lst): + return "\n".join(lst) diff --git a/core/templatetags/pretty.py b/core/templatetags/pretty.py new file mode 100644 index 0000000..74e7401 --- /dev/null +++ b/core/templatetags/pretty.py @@ -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") diff --git a/core/templatetags/urlsafe.py b/core/templatetags/urlsafe.py new file mode 100644 index 0000000..c673475 --- /dev/null +++ b/core/templatetags/urlsafe.py @@ -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="") diff --git a/stack.env.example b/stack.env.example new file mode 100644 index 0000000..b11951e --- /dev/null +++ b/stack.env.example @@ -0,0 +1,16 @@ +APP_PORT=5006 +REPO_DIR=. +APP_LOCAL_SETTINGS=./app/local_settings.py +APP_DATABASE_FILE=./db.sqlite3 +DOMAIN=dev.local +URL=http://127.0.0.1:5006 +ALLOWED_HOSTS=127.0.0.1,dev.local,zm.is,localhost,xf +NOTIFY_TOPIC=some-ntfy-topic +CSRF_TRUSTED_ORIGINS=http://127.0.0.1:5006,http://localhost:5006,http://qi:5006 +DEBUG=y +SECRET_KEY=asdkld00s0s0ds +STATIC_ROOT=/conf/static +REGISTRATION_OPEN=1 +OPERATION=dev +PROFILER=0 +BILLING_ENABLED=0