diff --git a/docs/dev/apps.md b/docs/dev/apps.md index 9489fbd..d50bdf5 100644 --- a/docs/dev/apps.md +++ b/docs/dev/apps.md @@ -2,3 +2,16 @@ The repapp is split into different modules, realized as Django apps, to improve the maintainability. +## one time login + +The one time login module provides a login for guests using a one time secret as part of an URL. + +### Settings + +- The templates use the `settings.ORGANIZATION` variable to fill the organization name. +- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host. + +## mail interface + + +## repapp members diff --git a/docs/dev/configuration.md b/docs/dev/configuration.md new file mode 100644 index 0000000..a0affae --- /dev/null +++ b/docs/dev/configuration.md @@ -0,0 +1,23 @@ +# Repapp configuration + +## Debug mode + +The Django debug mode is activated if there is a environment variable `DJANGO_DEBUG` with the value "true", "1" or "t". By default, the debug mode is disabled. + +## Organization name + +Some templates use and organization name. This value is taken from `settings.ORGANIZATION` and defaults to "Repair-Café Hilpoltstein". + +## Email settings + +The following environment variables are used to configure the Django email feature: + +``` Python +EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", "") +EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", "25")) +EMAIL_HOST_USER = os.getenv("DJANGO_EMAIL_HOST_USER", None) +EMAIL_HOST_PASSWORD = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None) +EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", "true") in ("true", "1", "t") +``` +See https://docs.djangoproject.com/en/4.2/topics/email/ for more details. + diff --git a/mkdocs.yml b/mkdocs.yml index 0952850..01fa306 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,6 +14,7 @@ nav: - 'Use cases': 'dev/use_cases.md' - Apps: 'dev/apps.md' - Objects: 'dev/objects.md' + - 'Repapp configuration': 'dev/configuration.md' - 'Repair-Café & Makespace': - Repair-Café: https://www.repaircafe-hilpoltstein.de - Makespace: https://makes-hacks-hip.de diff --git a/repapp/data/.gitkeep b/repapp/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/repapp/mail_inteface/__init__.py b/repapp/mail_inteface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/repapp/mail_inteface/admin.py b/repapp/mail_inteface/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/repapp/mail_inteface/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/repapp/mail_inteface/apps.py b/repapp/mail_inteface/apps.py new file mode 100644 index 0000000..9e9112a --- /dev/null +++ b/repapp/mail_inteface/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MailIntefaceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'mail_inteface' diff --git a/repapp/mail_inteface/migrations/__init__.py b/repapp/mail_inteface/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/repapp/mail_inteface/models.py b/repapp/mail_inteface/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/repapp/mail_inteface/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/repapp/mail_inteface/tests.py b/repapp/mail_inteface/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/repapp/mail_inteface/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/repapp/mail_inteface/views.py b/repapp/mail_inteface/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/repapp/mail_inteface/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/repapp/manage.py b/repapp/manage.py new file mode 100755 index 0000000..5e129a0 --- /dev/null +++ b/repapp/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'repapp.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/repapp/one_time_login/__init__.py b/repapp/one_time_login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/repapp/one_time_login/admin.py b/repapp/one_time_login/admin.py new file mode 100644 index 0000000..6dadf5d --- /dev/null +++ b/repapp/one_time_login/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from .models import OneTimeLogin + + +@admin.register(OneTimeLogin) +class OneTimeLoginAdmin(admin.ModelAdmin): + list_display = ['user', 'url', 'login_used', 'login_date', 'created'] + list_filter = ['user', 'url', 'login_used', 'login_date', 'created'] + search_fields = ['user', 'url'] + raw_id_fields = ['user'] + date_hierarchy = 'created' + ordering = ['created', 'user', 'url'] diff --git a/repapp/one_time_login/api.py b/repapp/one_time_login/api.py new file mode 100644 index 0000000..a8e737d --- /dev/null +++ b/repapp/one_time_login/api.py @@ -0,0 +1,64 @@ +import os +import datetime +import random +import logging +from hashlib import sha256 +from django.conf import settings +from django.contrib import messages +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ +from .models import OneTimeLogin + + +logger = logging.getLogger(__name__) + + +def create_one_time_login(user, url) -> str: + """ + create_one_time_login creates a one time login object for guest user logins. + """ + secret = sha256( + f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode( + 'utf-8') + ).hexdigest() + secret_hash = sha256(secret.encode('utf-8')).hexdigest() + otl = OneTimeLogin( + secret=secret_hash, + user=user, + url=url, + ) + otl.save() + return secret + + +def send_one_time_login_mail(secret, mail, request): + """ + Send a mail with a one time login link. + """ + url = request.build_absolute_uri( + f'/onetimelogin/{secret}/') + subject = render_to_string('one_time_login/mail_subject.txt', { + 'organization': settings.ORGANIZATION, + }).replace('\n', '') + html = render_to_string('one_time_login/mail_content.html', { + 'link': url, + 'organization': settings.ORGANIZATION, + }) + text = render_to_string('one_time_login/mail_content.html', { + 'link': url, + 'organization': settings.ORGANIZATION, + }) + + send_ok = send_mail( + subject=subject, + message=text, + from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""), + recipient_list=[mail], + fail_silently=True, + html_message=html + ) + + if send_ok > 0: + messages.add_message(request, messages.INFO, _( + 'A new login link was sent by mail.')) diff --git a/repapp/one_time_login/apps.py b/repapp/one_time_login/apps.py new file mode 100644 index 0000000..16de268 --- /dev/null +++ b/repapp/one_time_login/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OneTimeLoginConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'one_time_login' diff --git a/repapp/one_time_login/migrations/0001_initial.py b/repapp/one_time_login/migrations/0001_initial.py new file mode 100644 index 0000000..8ca3e66 --- /dev/null +++ b/repapp/one_time_login/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2 on 2023-08-27 19:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='OneTimeLogin', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('secret', models.CharField(max_length=200, unique=True, verbose_name='Secret')), + ('url', models.CharField(max_length=200, verbose_name='URL')), + ('created', models.DateField(default=django.utils.timezone.now, verbose_name='Creation date')), + ('login_used', models.BooleanField(default=False, verbose_name='Was the login used?')), + ('login_date', models.DateField(null=True, verbose_name='Login date')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'One time login', + 'verbose_name_plural': 'One time logins', + }, + ), + ] diff --git a/repapp/one_time_login/migrations/__init__.py b/repapp/one_time_login/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/repapp/one_time_login/models.py b/repapp/one_time_login/models.py new file mode 100644 index 0000000..7a0dded --- /dev/null +++ b/repapp/one_time_login/models.py @@ -0,0 +1,35 @@ +""" +Data models for the one time login feature. +""" + +import django.utils.timezone +from django.db import models +from django.conf import settings +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + + +class OneTimeLogin(models.Model): + """ + A one time login (otl) is a secret which can be used once to login a user. + """ + secret = models.CharField(max_length=200, verbose_name=_( + "Secret"), unique=True, null=False) + url = models.CharField(max_length=200, verbose_name=_("URL")) + created = models.DateField(verbose_name=_( + "Creation date"), default=django.utils.timezone.now) + login_used = models.BooleanField( + verbose_name=_("Was the login used?"), default=False) + login_date = models.DateField(verbose_name=_("Login date"), null=True) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False, verbose_name=_("User")) + + class Meta: + verbose_name = _('One time login') + verbose_name_plural = _('One time logins') + + def __str__(self): + return f'One time login for {self.user}' + + def get_absolute_url(self): + return reverse('one_time_login:login', args=[self.secret]) diff --git a/repapp/one_time_login/templates/one_time_login/mail_content.html b/repapp/one_time_login/templates/one_time_login/mail_content.html new file mode 100644 index 0000000..3ea3845 --- /dev/null +++ b/repapp/one_time_login/templates/one_time_login/mail_content.html @@ -0,0 +1,19 @@ + + +
+ + + +Hello,
++ we prepared a one time login for you. Click here to use the link. +
++ Kind regards, + {{ organization }} +
+ + diff --git a/repapp/one_time_login/templates/one_time_login/mail_content.txt b/repapp/one_time_login/templates/one_time_login/mail_content.txt new file mode 100644 index 0000000..2cea407 --- /dev/null +++ b/repapp/one_time_login/templates/one_time_login/mail_content.txt @@ -0,0 +1,6 @@ +Hello, + +we prepared a one time login for you. Open {{ link }} in your browser to use the link. + +Kind regards, +{{ organization }} diff --git a/repapp/one_time_login/templates/one_time_login/mail_subject.txt b/repapp/one_time_login/templates/one_time_login/mail_subject.txt new file mode 100644 index 0000000..5f06fde --- /dev/null +++ b/repapp/one_time_login/templates/one_time_login/mail_subject.txt @@ -0,0 +1 @@ +{{ organization }} - Einmal-Ammeldelink \ No newline at end of file diff --git a/repapp/one_time_login/tests.py b/repapp/one_time_login/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/repapp/one_time_login/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/repapp/one_time_login/urls.py b/repapp/one_time_login/urls.py new file mode 100644 index 0000000..2df01bf --- /dev/null +++ b/repapp/one_time_login/urls.py @@ -0,0 +1,13 @@ +""" +Urls of RepApp. +""" +from django.urls import path +from . import views + + +app_name = 'one_time_login' + + +urlpatterns = [ + path("