implement user logins

This commit is contained in:
Tom Irgang 2023-04-24 18:02:43 +02:00
parent 050dc4cd41
commit 9a8dd3f9fd
43 changed files with 679 additions and 890 deletions

View file

@ -9,7 +9,15 @@
"reparateur",
"organisator",
"Reparateurs",
"repapp"
"repapp",
"oidc",
"anmeldung",
"repaircafe",
"hilpoltstein",
"KeycloakOIDCAB",
"Keycloak",
"tabindex",
"onetimelogin"
],
"ignoreWords": [],
"import": []

View file

@ -290,3 +290,32 @@ Als bekannter Gast der ein defekten Geräte anmelden möchte,
- Die nächste Seite (S2) enthält ein Formular mit den Feldern `eMail-Adresse`, `Art des Geräts`, `Hersteller & Modell/Typ`, `Fehlerbeschreibung`, `Foto vom Gerät`, `Foto vom Typenschild`, ein Kontrollkästchen `Folgetermin`, ein Kontrollkästchen `Informationen zur Reparaturabwicklung` une ein Kontrollkästchen `Datenschutz`. Nachdem ich die Felder ausgefüllt habe klicke auf auf `Absenden`. Dieser Klick bringt mich auf die nächste Seite.
- Die nächste Seite (S5) bestätigt mir die Anmeldung meines Gerätes.
- In meinem eMail Posteingang finde ich ebenfalls eine Bestätigung der Geräteanmeldung.
## Konfigurationsparameter
- DJANGO_SECRET_KEY
- DJANGO_DEBUG
- DJANGO_EMAIL_HOST
- DJANGO_EMAIL_PORT
- DJANGO_EMAIL_HOST_USER
- DJANGO_EMAIL_HOST_PASSWORD
- DJANGO_EMAIL_USE_TLS
- OIDC_RP_CLIENT_SECRET
### Feste Konfiguration
- ALLOWED_HOSTS
- LANGUAGE_CODE
- TIME_ZONE
- CSRF_TRUSTED_ORIGINS
- AUTH_USER_MODEL
- AUTHENTICATION_BACKENDS
- LOGIN_REDIRECT_URL
- OIDC_RP_CLIENT_ID
- OIDC_RP_SIGN_ALGO
- OIDC_OP_JWKS_ENDPOINT
- OIDC_OP_AUTHORIZATION_ENDPOINT
- OIDC_OP_TOKEN_ENDPOINT
- OIDC_OP_USER_ENDPOINT
- LOGIN_REDIRECT_URL
- LOGOUT_REDIRECT_URL

View file

@ -1,7 +1,7 @@
"""
Django settings for rc_hip project.
Generated by 'django-admin startproject' using Django 4.2.
Generated by "django-admin startproject" using Django 4.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
@ -15,7 +15,7 @@ from pathlib import Path
mimetypes.add_type("text/css", ".css", True)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# Build paths inside the project like this: BASE_DIR / "subdir".
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_ROOT = os.path.join(BASE_DIR, "static")
@ -25,77 +25,77 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# SECRET_KEY = 'django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*'
SECRET_KEY = os.environ.get(
"DJANGO_SECRET_KEY", 'django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*')
# SECRET_KEY = "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*"
SECRET_KEY = os.getenv(
"DJANGO_SECRET_KEY", "django-insecure-ura&_-(r(&&b$0s!a=07c@k6c!8=8m7$5(4#w8m@79-e%rn6s*")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DJANGO_DEBUG", 'true').lower() in ('true', '1', 't')
# SECURITY WARNING: don"t run with debug turned on in production!
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() in ("true", "1", "t")
ALLOWED_HOSTS = [
'127.0.0.1',
'localhost',
'repapp.rc-hip.de'
"127.0.0.1",
"localhost",
"repapp.rc-hip.de",
"anmeldung.repaircafe-hilpoltstein.de",
]
# Application definition
INSTALLED_APPS = [
'repapp.apps.RepappConfig',
'django.contrib.admin',
'django.contrib.auth',
'mozilla_django_oidc',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'crispy_bootstrap4',
'repapp_users.apps.RepappUsersConfig',
'import_export',
"repapp.apps.RepappConfig",
"django.contrib.admin",
"django.contrib.auth",
"mozilla_django_oidc",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_forms",
"crispy_bootstrap4",
"import_export",
]
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap4'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap4"
CRISPY_TEMPLATE_PACK = "bootstrap4"
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'rc_hip.urls'
ROOT_URLCONF = "rc_hip.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'rc_hip.wsgi.application'
WSGI_APPLICATION = "rc_hip.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data' / 'db.sqlite3',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "data" / "db.sqlite3",
}
}
@ -105,16 +105,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
@ -122,9 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'de'
LANGUAGE_CODE = "de"
TIME_ZONE = 'Europe/Berlin'
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
@ -134,35 +134,41 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
MEDIA_URL = 'media/'
STATIC_URL = "static/"
MEDIA_URL = "media/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CSRF_TRUSTED_ORIGINS = [
'https://repapp.rc-hip.de',
'http://127.0.0.1:8020'
"http://127.0.0.1:8020",
"http://localhost:8020",
"http://127.0.0.1:8000",
"http://localhost:8000",
"https://repapp.rc-hip.de",
"https://anmeldung.repaircafe-hilpoltstein.de",
]
EMAIL_HOST = os.getenv("DJANGO_EMAIL_HOST", '')
EMAIL_PORT = (int)(os.getenv("DJANGO_EMAIL_PORT", '25'))
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')
EMAIL_USE_TLS = os.getenv("DJANGO_EMAIL_USE_TLS", "true") in ("true", "1", "t")
AUTH_USER_MODEL = "repapp.CustomUser"
AUTH_USER_MODEL = "repapp_users.CustomUser"
AUTHENTICATION_BACKENDS = [
"repapp_users.backends.EmailBackend",
"repapp.backends.EmailBackend",
"django.contrib.auth.backends.ModelBackend",
# 'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
'repapp_users.backends.KeycloakOIDCAB'
# "mozilla_django_oidc.auth.OIDCAuthenticationBackend",
"repapp.backends.KeycloakOIDCAB",
"repapp.backends.OneTimeLoginBackend",
]
LOGIN_REDIRECT_URL = '/guest/profile/'
LOGIN_REDIRECT_URL = "/guest/profile/"
OIDC_RP_CLIENT_ID = "repapp"
OIDC_RP_CLIENT_SECRET = os.getenv("DJANGO_OIDC_RP_CLIENT_SECRET", None)

View file

@ -4,12 +4,18 @@ Admin interface configuration for RepApp.
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin, ExportActionMixin
from import_export import resources
from .models import (Organisator, Cafe, Question, Device,
Reparateur, Appointment, Guest, Candidate)
from .models import (Organisator, Cafe, Question, Device, CustomUser,
Reparateur, Appointment, Guest, Candidate, OneTimeLogin)
class GuestResource(resources.ModelResource):
"""
Resource wrapper for Guest model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Guest
@staticmethod
@ -18,7 +24,13 @@ class GuestResource(resources.ModelResource):
class DeviceResource(resources.ModelResource):
"""
Resource wrapper for Device model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Device
@staticmethod
@ -27,7 +39,13 @@ class DeviceResource(resources.ModelResource):
class QuestionResource(resources.ModelResource):
"""
Resource wrapper for Question model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Question
@staticmethod
@ -36,7 +54,13 @@ class QuestionResource(resources.ModelResource):
class AppointmentResource(resources.ModelResource):
"""
Resource wrapper for Appointment model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Appointment
@staticmethod
@ -45,7 +69,13 @@ class AppointmentResource(resources.ModelResource):
class CandidateResource(resources.ModelResource):
"""
Resource wrapper for Candidate model.
"""
class Meta:
"""
Meta data for resource model.
"""
model = Candidate
@staticmethod
@ -54,6 +84,9 @@ class CandidateResource(resources.ModelResource):
class ExportAdmin(ImportExportModelAdmin, ExportActionMixin):
"""
Admin model for data export, supporting buttons and action.
"""
resource_classes = [
DeviceResource,
GuestResource,
@ -71,3 +104,5 @@ admin.site.register(Device, ExportAdmin)
admin.site.register(Reparateur)
admin.site.register(Appointment, ExportAdmin)
admin.site.register(Candidate, ExportAdmin)
admin.site.register(CustomUser)
admin.site.register(OneTimeLogin)

View file

@ -1,6 +1,12 @@
"""
RepApp application.
"""
from django.apps import AppConfig
class RepappConfig(AppConfig):
"""
Django config for RepApp application.
"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'repapp'

65
rc_hip/repapp/backends.py Normal file
View file

@ -0,0 +1,65 @@
"""
Authentication backends for RepApp.
"""
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from .utils import generate_username, create_repapp_user
from .models import OneTimeLogin
class OneTimeLoginBackend(ModelBackend):
"""
OneTimeLoginBackend implements a login using a onetime secret.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
print(f'authenticate {username}')
login = OneTimeLogin.objects.filter(secret=username).first()
if login:
return login.user
else:
return None
class EmailBackend(ModelBackend):
"""
EmailBackend allows a login using email address as username.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
userModel = get_user_model()
try:
user = userModel.objects.get(email=username)
except userModel.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
class KeycloakOIDCAB(OIDCAuthenticationBackend):
"""
KeycloakOIDCAB allows a login using Open ID Connect (with the Repair-Café Keycloak Single Sign On server)
"""
def create_user(self, claims):
user = super(KeycloakOIDCAB, self).create_user(claims)
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.save()
create_repapp_user(user)
return user
def update_user(self, user, claims):
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.save()
create_repapp_user(user)
return user

View file

@ -1,3 +1,6 @@
"""
Django forms for RepApp.
"""
from django import forms
from django.core.exceptions import ValidationError

View file

@ -1,7 +1,12 @@
# Generated by Django 4.2 on 2023-04-16 17:49
# Generated by Django 4.2 on 2023-04-24 15:16
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import repapp.models
class Migration(migrations.Migration):
@ -9,54 +14,170 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Cafe',
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ort', models.CharField(max_length=200)),
('datum', models.DateField()),
('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')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse')),
('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': 'Benutzer',
'verbose_name_plural': 'Benutzer',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Gerät',
name='Cafe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200)),
('besitzer', models.CharField(max_length=200)),
('mail', models.CharField(max_length=200)),
('gerät', models.CharField(max_length=200)),
('fehler', models.TextField()),
('folgetermin', models.BooleanField()),
('location', models.CharField(max_length=200, verbose_name='Ort')),
('address', models.CharField(max_length=200, verbose_name='Adresse')),
('event_date', models.DateField(verbose_name='Datum')),
],
options={
'verbose_name': 'Repair-Café',
'verbose_name_plural': 'Repair-Cafés',
},
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('device', models.CharField(max_length=200, verbose_name='Art des Geräts')),
('manufacturer', models.CharField(max_length=200, verbose_name='Hersteller & Modell/Typ')),
('error', models.TextField(verbose_name='Fehlerbeschreibung')),
('follow_up', models.BooleanField(verbose_name='Folgetermin')),
('device_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Gerät')),
('type_plate_picture', models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Typenschild')),
('confirmed', models.BooleanField(verbose_name='Bestätigung gesendet?')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
],
options={
'verbose_name': 'Gerät',
'verbose_name_plural': 'Geräte',
},
),
migrations.CreateModel(
name='Guest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('phone', models.CharField(max_length=200, verbose_name='Telefonnummer')),
('residence', models.CharField(max_length=200, verbose_name='Wohnort')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Gast',
'verbose_name_plural': 'Gäste',
},
),
migrations.CreateModel(
name='Organisator',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('mail', models.CharField(max_length=200)),
('name', models.CharField(max_length=200, verbose_name='Name')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Organisator',
'verbose_name_plural': 'Organisatoren',
},
),
migrations.CreateModel(
name='Reparateur',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('mail', models.CharField(max_length=200)),
('name', models.CharField(max_length=200, verbose_name='Name')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Reparateur',
'verbose_name_plural': 'Reparateure',
},
),
migrations.CreateModel(
name='Termin',
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uhrzeit', models.TimeField()),
('bestätigt', models.BooleanField()),
('cafeid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.cafe')),
('gerätid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.gerät')),
('reparateurid', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='repapp.reparateur')),
('question', models.TextField(verbose_name='Frage')),
('answer', models.TextField(verbose_name='Antwort')),
('date', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellungsdatum')),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
('organisator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator', verbose_name='Organisator')),
('reparateur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')),
],
options={
'verbose_name': 'Frage',
'verbose_name_plural': 'Fragen',
},
),
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='Erstellungsdatum')),
('login_used', models.BooleanField(default=False, verbose_name='Login benutzt?')),
('login_date', models.DateField(null=True, verbose_name='Login Datum')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Benutzer')),
],
options={
'verbose_name': 'Einmal-Login',
'verbose_name_plural': 'Einmal-Login',
},
),
migrations.AddField(
model_name='device',
name='guest',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'),
),
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
],
options={
'verbose_name': 'Kandidat',
'verbose_name_plural': 'Kandidaten',
},
),
migrations.CreateModel(
name='Appointment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.TimeField(verbose_name='Zeit')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
('cafe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
('reparateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur')),
],
options={
'verbose_name': 'Termin',
'verbose_name_plural': 'Termine',
},
),
]

View file

@ -1,41 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 17:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='termin',
name='cafeid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe'),
),
migrations.AlterField(
model_name='termin',
name='gerätid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät'),
),
migrations.AlterField(
model_name='termin',
name='reparateurid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur'),
),
migrations.CreateModel(
name='Frage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('frage', models.TextField()),
('antwort', models.TextField()),
('datum', models.DateField()),
('gerätid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät')),
('organisatorid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator')),
('reparateurid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur')),
],
),
]

View file

@ -1,29 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 18:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0002_alter_termin_cafeid_alter_termin_gerätid_and_more'),
]
operations = [
migrations.AlterField(
model_name='frage',
name='organisatorid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator'),
),
migrations.AlterField(
model_name='frage',
name='reparateurid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur'),
),
migrations.AlterField(
model_name='termin',
name='gerätid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.gerät'),
),
]

View file

@ -1,153 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 18:44
import datetime
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0003_alter_frage_organisatorid_alter_frage_reparateurid_and_more'),
]
operations = [
migrations.CreateModel(
name='Appointment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('time', models.TimeField(verbose_name='Zeit')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
],
options={
'verbose_name': 'Termin',
'verbose_name_plural': 'Termine',
},
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('owner', models.CharField(max_length=200, verbose_name='Besitzer')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
('device', models.CharField(max_length=200, verbose_name='Gerät')),
('error', models.TextField(verbose_name='Fehler')),
('follow_up', models.BooleanField(verbose_name='Folgetermin')),
],
options={
'verbose_name': 'Gerät',
'verbose_name_plural': 'Geräte',
},
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.TextField(verbose_name='Frage')),
('answer', models.TextField(verbose_name='Antwort')),
('date', models.DateField(verbose_name='Erstellungsdatum')),
('deviceid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
],
options={
'verbose_name': 'Frage',
'verbose_name_plural': 'Fragen',
},
),
migrations.RemoveField(
model_name='termin',
name='cafeid',
),
migrations.RemoveField(
model_name='termin',
name='gerätid',
),
migrations.RemoveField(
model_name='termin',
name='reparateurid',
),
migrations.AlterModelOptions(
name='cafe',
options={'verbose_name': 'Repair-Café', 'verbose_name_plural': 'Repair-Cafés'},
),
migrations.AlterModelOptions(
name='organisator',
options={'verbose_name': 'Organisator', 'verbose_name_plural': 'Organisatoren'},
),
migrations.AlterModelOptions(
name='reparateur',
options={'verbose_name': 'Reparateur', 'verbose_name_plural': 'Reparateure'},
),
migrations.RemoveField(
model_name='cafe',
name='datum',
),
migrations.RemoveField(
model_name='cafe',
name='ort',
),
migrations.AddField(
model_name='cafe',
name='event_date',
field=models.DateField(default=datetime.date(2023, 4, 16), verbose_name='Datum'),
),
migrations.AddField(
model_name='cafe',
name='location',
field=models.CharField(default='', max_length=200, verbose_name='Ort'),
),
migrations.AlterField(
model_name='organisator',
name='mail',
field=models.CharField(max_length=200, verbose_name='eMail'),
),
migrations.AlterField(
model_name='organisator',
name='name',
field=models.CharField(max_length=200, verbose_name='Name'),
),
migrations.AlterField(
model_name='reparateur',
name='mail',
field=models.CharField(max_length=200, verbose_name='eMail'),
),
migrations.AlterField(
model_name='reparateur',
name='name',
field=models.CharField(max_length=200, verbose_name='Name'),
),
migrations.DeleteModel(
name='Frage',
),
migrations.DeleteModel(
name='Gerät',
),
migrations.DeleteModel(
name='Termin',
),
migrations.AddField(
model_name='question',
name='organisatorid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.organisator', verbose_name='Organisator'),
),
migrations.AddField(
model_name='question',
name='reparateurid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur'),
),
migrations.AddField(
model_name='appointment',
name='cafeid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café'),
),
migrations.AddField(
model_name='appointment',
name='deviceid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät'),
),
migrations.AddField(
model_name='appointment',
name='reparateurid',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.reparateur', verbose_name='Reparateur'),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 18:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0004_appointment_device_question_remove_termin_cafeid_and_more'),
]
operations = [
migrations.AlterField(
model_name='cafe',
name='event_date',
field=models.DateField(verbose_name='Datum'),
),
migrations.AlterField(
model_name='cafe',
name='location',
field=models.CharField(max_length=200, verbose_name='Ort'),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 19:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0005_alter_cafe_event_date_alter_cafe_location'),
]
operations = [
migrations.AddField(
model_name='device',
name='phone',
field=models.CharField(default='', max_length=200, verbose_name='Telefonnummer'),
),
migrations.AddField(
model_name='device',
name='residence',
field=models.CharField(default='', max_length=200, verbose_name='Wohnort'),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.2 on 2023-04-16 19:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0006_device_phone_device_residence'),
]
operations = [
migrations.AlterField(
model_name='device',
name='phone',
field=models.CharField(max_length=200, verbose_name='Telefonnummer'),
),
migrations.AlterField(
model_name='device',
name='residence',
field=models.CharField(max_length=200, verbose_name='Wohnort'),
),
]

View file

@ -1,50 +0,0 @@
# Generated by Django 4.2 on 2023-04-17 05:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0007_alter_device_phone_alter_device_residence'),
]
operations = [
migrations.CreateModel(
name='Guest',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.CharField(max_length=200, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('phone', models.CharField(max_length=200, verbose_name='Telefonnummer')),
('residence', models.CharField(max_length=200, verbose_name='Wohnort')),
('mail', models.CharField(max_length=200, verbose_name='eMail')),
],
options={
'verbose_name': 'Gast',
'verbose_name_plural': 'Gäste',
},
),
migrations.RemoveField(
model_name='device',
name='mail',
),
migrations.RemoveField(
model_name='device',
name='owner',
),
migrations.RemoveField(
model_name='device',
name='phone',
),
migrations.RemoveField(
model_name='device',
name='residence',
),
migrations.AddField(
model_name='device',
name='guestid',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.guest', verbose_name='Gast'),
),
]

View file

@ -1,48 +0,0 @@
# Generated by Django 4.2 on 2023-04-17 07:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('repapp', '0008_guest_remove_device_mail_remove_device_owner_and_more'),
]
operations = [
migrations.RenameField(
model_name='appointment',
old_name='cafeid',
new_name='cafe_id',
),
migrations.RenameField(
model_name='appointment',
old_name='deviceid',
new_name='device_id',
),
migrations.RenameField(
model_name='appointment',
old_name='reparateurid',
new_name='reparateur_id',
),
migrations.RenameField(
model_name='device',
old_name='guestid',
new_name='guest_id',
),
migrations.RenameField(
model_name='question',
old_name='deviceid',
new_name='device_id',
),
migrations.RenameField(
model_name='question',
old_name='organisatorid',
new_name='organisator_id',
),
migrations.RenameField(
model_name='question',
old_name='reparateurid',
new_name='reparateur_id',
),
]

View file

@ -1,27 +0,0 @@
# Generated by Django 4.2 on 2023-04-17 07:57
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0009_rename_cafeid_appointment_cafe_id_and_more'),
]
operations = [
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('confirmed', models.BooleanField(verbose_name='bestätigt')),
('cafe_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café')),
('device_id', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='repapp.device', verbose_name='Gerät')),
],
options={
'verbose_name': 'Kandidat',
'verbose_name_plural': 'Kandidaten',
},
),
]

View file

@ -1,80 +0,0 @@
# Generated by Django 4.2 on 2023-04-20 18:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('repapp', '0010_candidate'),
]
operations = [
migrations.RenameField(
model_name='appointment',
old_name='cafe_id',
new_name='cafe',
),
migrations.RenameField(
model_name='appointment',
old_name='device_id',
new_name='device',
),
migrations.RenameField(
model_name='appointment',
old_name='reparateur_id',
new_name='reparateur',
),
migrations.RenameField(
model_name='candidate',
old_name='cafe_id',
new_name='cafe',
),
migrations.RenameField(
model_name='candidate',
old_name='device_id',
new_name='device',
),
migrations.RenameField(
model_name='device',
old_name='guest_id',
new_name='guest',
),
migrations.RenameField(
model_name='question',
old_name='device_id',
new_name='device',
),
migrations.RenameField(
model_name='question',
old_name='organisator_id',
new_name='organisator',
),
migrations.RenameField(
model_name='question',
old_name='reparateur_id',
new_name='reparateur',
),
migrations.AddField(
model_name='device',
name='cafe',
field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='repapp.cafe', verbose_name='Repair-Café'),
preserve_default=False,
),
migrations.AddField(
model_name='device',
name='confirmed',
field=models.BooleanField(default=False, verbose_name='Bestätigt'),
),
migrations.AddField(
model_name='device',
name='secret',
field=models.CharField(default='TEST12345', max_length=200, verbose_name='Bestätigungscode'),
),
migrations.AddField(
model_name='guest',
name='confirmed',
field=models.BooleanField(default=False, verbose_name='Bestätigt'),
),
]

View file

@ -1,49 +0,0 @@
# Generated by Django 4.2 on 2023-04-21 15:21
from django.db import migrations, models
import repapp.models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0011_rename_cafe_id_appointment_cafe_and_more'),
]
operations = [
migrations.RemoveField(
model_name='device',
name='secret',
),
migrations.RemoveField(
model_name='guest',
name='confirmed',
),
migrations.AddField(
model_name='cafe',
name='address',
field=models.CharField(default='', max_length=200, verbose_name='Ort'),
preserve_default=False,
),
migrations.AddField(
model_name='device',
name='device_picture',
field=models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Gerät'),
),
migrations.AddField(
model_name='device',
name='manufacturer',
field=models.CharField(default='', max_length=200, verbose_name='Hersteller'),
preserve_default=False,
),
migrations.AddField(
model_name='device',
name='type_plate_picture',
field=models.FileField(null=True, upload_to=repapp.models.device_directory_path, verbose_name='Foto vom Typenschild'),
),
migrations.AlterField(
model_name='device',
name='confirmed',
field=models.BooleanField(verbose_name='Bestätigung gesendet?'),
),
]

View file

@ -1,35 +0,0 @@
# Generated by Django 4.2 on 2023-04-23 10:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp', '0012_remove_device_secret_remove_guest_confirmed_and_more'),
]
operations = [
migrations.AlterField(
model_name='cafe',
name='address',
field=models.CharField(max_length=200, verbose_name='Adresse'),
),
migrations.AlterField(
model_name='device',
name='device',
field=models.CharField(
max_length=200, verbose_name='Art des Geräts'),
),
migrations.AlterField(
model_name='device',
name='error',
field=models.TextField(verbose_name='Fehlerbeschreibung'),
),
migrations.AlterField(
model_name='device',
name='manufacturer',
field=models.CharField(
max_length=200, verbose_name='Hersteller & Modell/Typ'),
),
]

View file

@ -4,6 +4,7 @@ This module implements the models of RepApp.
import django.utils.timezone
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import AbstractUser
class Cafe(models.Model):
@ -73,6 +74,9 @@ class Guest(models.Model):
def device_directory_path(instance, filename):
"""
device_directory_path generates a device-specific storage path for file uploads.
"""
return f'device_{instance.identifier}/{filename}'
@ -168,3 +172,38 @@ class Candidate(models.Model):
def __str__(self):
return f'Kandidat {self.cafe.event_date} für Gerät {self.device.device}'
class CustomUser(AbstractUser):
email = models.EmailField(unique=True, verbose_name=_("eMail Adresse"))
class Meta:
verbose_name = _('Benutzer')
verbose_name_plural = _('Benutzer')
def __str__(self):
return f'{self.email}'
class OneTimeLogin(models.Model):
"""
A OneTimeLogin 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)
user = models.ForeignKey(
CustomUser, on_delete=models.CASCADE, null=False, verbose_name=_("Benutzer"))
url = models.CharField(max_length=200, verbose_name=_("URL"))
created = models.DateField(verbose_name=_(
"Erstellungsdatum"), default=django.utils.timezone.now)
login_used = models.BooleanField(
verbose_name=_("Login benutzt?"), default=False)
login_date = models.DateField(verbose_name=_(
"Login Datum"), null=True)
class Meta:
verbose_name = _('Einmal-Login')
verbose_name_plural = _('Einmal-Login')
def __str__(self):
return f'Einmal-Login {self.user.email}'

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="description" content="Repair-Café Hilpoltstein - Benutzerkonto" />
<meta name="keywords" content="Benutzerkonto" />
<title>Repair-Café Hilpoltstein - Benutzerkonto</title>
</head>
<body>
<p>Guten Tag,</p>
<p>
wir haben ein Benutzerkonto für Sie erstellt. Klicken sie <a href="{{ link }}">hier</a> um ihre Daten einzusehen und zu ändern.
</p>
<p>Ihr Anmeldedaten sind:</p>
<p>Benutzername: {{ username }}</p>
<p>Passwort: {{ password }}</p>
<p>Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!</p>
</body>
</html>

View file

@ -0,0 +1 @@
Repair-Café Hilpoltstein - Benutzerkonto

View file

@ -0,0 +1,6 @@
Guten Tag,
wir haben ein Benutzerkonto für Sie erstellt. Sie können sich im Web-Broswer unter {{ link }} um ihre Daten einzusehen und zu ändern.
Ihr Anmeldedaten sind:
Benutzername: {{ username }}
Passwort: {{ password }}
Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="description" content="Einmal-Anmeldelink" />
<meta name="keywords" content="Login" />
<title>Einmal-Anmeldelink</title>
</head>
<body>
<p>Guten Tag,</p>
<p>
wir haben für Sie einen Einmal-Ammeldelink erstellt. Wenn sie <a href="{{ link }}">hier</a> klicken können sie auf ihre Informationen zugreifen.
</p>
<p>Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!</p>
</body>
</html>

View file

@ -0,0 +1 @@
Repair-Café Hilpoltstein - Einmal-Ammeldelink

View file

@ -0,0 +1,3 @@
Guten Tag,
wir haben für Sie einen Einmal-Ammeldelink erstellt. Wenn sie {{ link }} im Web-Broswer öffnen können sie auf ihre Informationen zugreifen.
Herzliche Grüße sendet dir das Organisationsteam vom Repair-Café Hilpoltstein!

View file

@ -1,5 +1,11 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="description" content="Repair-Café Hilpoltstein Geräteanmeldung" />
<meta name="keywords" content="Geräteanmeldung" />
<title>Repair-Café Hilpoltstein Geräteanmeldung</title>
</head>
<body>
<p>Guten Tag,</p>
<p>
@ -8,6 +14,9 @@
<p>
Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}.
</p>
<p>
Sie können die Daten zu ihrer Geräteanmeldung <a href="{{ login_url }}">hier</a> einsehen.
</p>
<p>
Die Organisation unserer Repair-Cafés beginnt i.a. einige Tage vor dem jeweiligen Termin. Dann erstellen wir anhand der angemeldeten Geräte und der Spezialgebiete der anwesenden Reparateure einen Zeitplan.
</p>

View file

@ -1,6 +1,7 @@
Guten Tag,
dies ist eine automatische Nachricht, die du erhältst, nachdem du unser Anmeldeformular abgesendet hast und deine Angaben in unserer Datenbank gespeichert wurden.
Vielen Dank für die Anmeldung des Geräts {{ device.device }} - {{ device.manufacturer }} zur Reparatur im Repair-Café am {{ cafe.event_date|date }}.
Sie können die Daten zu ihrer Geräteanmeldung <a href="{{ login_url }}">hier</a> einsehen.
Die Organisation unserer Repair-Cafés beginnt i.a. einige Tage vor dem jeweiligen Termin. Dann erstellen wir anhand der angemeldeten Geräte und der Spezialgebiete der anwesenden Reparateure einen Zeitplan.
Sobald dieser Zeitplan fertig gestellt ist, erhältst du von einem Mitglied unseres Organisationsteams eine Mail. Diese beinhaltet im Normalfall den Reparaturtermin inkl. der Uhrzeit, für die die Reparatur vorgesehen ist. In Ausnahmefällen (z.B. wenn kein Spezialist für die Gerätegruppe anwesend sein wird oder überbucht ist) ist es aber auch möglich, dass wir deinen gewünschten Termin absagen und dir einen Alternativtermin vorschlagen müssen.
Falls du in der Zwischenzeit Rückfragen hast oder deinen Wunschtermin doch nicht wahrnehmen kannst, antworte bitte einfach auf diese Nachricht.

View file

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here.

View file

@ -2,7 +2,6 @@
Urls of RepApp.
"""
from django.urls import path
from . import views
urlpatterns = [
@ -25,4 +24,8 @@ urlpatterns = [
views.profile, name="guest_profile"),
path("member/login/",
views.member_login, name="member_login"),
path("cron", views.cron, name="cron"),
path("process_mails", views.process_mails, name="process_mails"),
path("onetimelogin/<str:secret>/",
views.one_time_login, name="one_time_login"),
]

171
rc_hip/repapp/utils.py Normal file
View file

@ -0,0 +1,171 @@
import os
import unicodedata
import datetime
import random
from hashlib import sha256
from django.template.loader import render_to_string
from django.urls import reverse
from django.core.mail import send_mail
from django.contrib import messages
from repapp.models import Organisator, Reparateur, OneTimeLogin
def create_one_time_login(user, url):
"""
create_one_time_login create a one time login object for user logins.
"""
secret = sha256(
f'{user.email}{url}{datetime.datetime.now()}{random.randint(0,9999999)}'.encode(
'utf-8')
).hexdigest()
otl = OneTimeLogin(
secret=secret,
user=user,
url=url,
)
otl.save()
return otl
def send_one_time_login_mail(one_time_login, request):
url = request.build_absolute_uri(
f'/onetimelogin/{one_time_login.secret}/')
subject = render_to_string(
'repapp/mail/mail_one_time_login_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_one_time_login_text.html', {
'link': url,
})
html = render_to_string('repapp/mail/mail_one_time_login_html.html', {
'link': url,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{one_time_login.user.email}"],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben einen neuen Login per eMail erhalten.')
def create_repapp_user(user):
organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator:
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
reparateur.name = user.username
reparateur.save()
else:
organisator.name = user.username
organisator.save()
def generate_username(email):
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
def send_confirmation_mails(device, guest, cafe, request):
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
text = render_to_string('repapp/mail/notice_new_device.html', {
'guest': device.guest,
'device': device,
'cafe': device.cafe,
})
send_mail(
subject=f"Neues Gerät { device.device }",
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=organizers,
fail_silently=True
)
path = reverse('view_device', kwargs={
'device_identifier': device.identifier})
url = request.build_absolute_uri(path)
otl = create_one_time_login(guest.user, url)
login_url = request.build_absolute_uri(f'/onetimelogin/{otl.secret}/')
subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
'cafe': cafe,
}).replace('\n', '')
text = render_to_string('repapp/mail/mail_register_device_text.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
html = render_to_string('repapp/mail/mail_register_device_html.html', {
'guest': guest,
'device': device,
'cafe': cafe,
'login_url': login_url,
})
mail_count = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.mail}"],
fail_silently=True,
html_message=html
)
if mail_count > 0:
device.confirmed = True
device.save()
else:
messages.add_message(request, messages.ERROR,
'Fehler beim senden der Bestätigungs-eMail!')
def send_guest_account_mail(guest, password, request):
url = request.build_absolute_uri('/guest/profile/')
subject = render_to_string(
'repapp/mail/mail_new_guest_subject.html').replace('\n', '')
text = render_to_string('repapp/mail/mail_new_guest_text.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
html = render_to_string('repapp/mail/mail_new_guest_html.html', {
'link': url,
'username': guest.user.email,
'password': password,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.user.email}"],
fail_silently=True,
html_message=html
)
if ok > 0:
messages.add_message(request, messages.INFO,
'Sie haben ihre Benutzerdaten per eMail erhalten.')
def is_member(user):
organisator = Organisator.objects.filter(mail=user.email).first()
reparateur = Reparateur.objects.filter(mail=user.email).first()
return organisator or reparateur

View file

@ -2,9 +2,9 @@
Views of RepApp.
"""
import datetime
import os
import random
import string
import time
from hashlib import sha256
from django.views import generic
from django.urls import reverse_lazy
@ -12,12 +12,14 @@ from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.contrib.auth.decorators import login_required
from repapp_users.models import CustomUser
from .models import Cafe, Device, Guest, Organisator
from django.contrib.auth import authenticate, login
from django.utils.timezone import now
from repapp.models import CustomUser
from .models import Cafe, Device, Guest, OneTimeLogin
from .forms import RegisterDevice, RegisterGuest
from .utils import (send_confirmation_mails, create_one_time_login,
send_one_time_login_mail, send_guest_account_mail, is_member)
class IndexView(generic.ListView):
@ -32,59 +34,10 @@ class IndexView(generic.ListView):
return Cafe.objects.filter(event_date__gte=datetime.date.today())
def send_confirmation_mails(device, guest, cafe, request):
organizers = []
for organizer in Organisator.objects.all():
organizers.append(organizer.mail)
text = render_to_string('repapp/mail/notice_new_device.html', {
'guest': device.guest,
'device': device,
'cafe': device.cafe,
})
send_mail(
subject=f"Neues Gerät { device.device }",
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=organizers,
fail_silently=True
)
subject = render_to_string('repapp/mail/mail_register_device_subject.html', {
'guest': guest,
'device': device,
'cafe': cafe,
}).replace('\n', '')
text = render_to_string('repapp/mail/mail_register_device_text.html', {
'guest': guest,
'device': device,
'cafe': cafe,
})
html = render_to_string('repapp/mail/mail_register_device_html.html', {
'guest': guest,
'device': device,
'cafe': cafe,
})
ok = send_mail(
subject=subject,
message=text,
from_email=os.getenv("DJANGO_SENDER_ADDRESS", ""),
recipient_list=[f"{guest.mail}"],
fail_silently=True,
html_message=html
)
if ok > 0:
device.confirmed = True
device.save()
else:
messages.add_message(request, messages.ERROR,
'Fehler beim senden der Bestätigungs-eMail!')
class RegisterDeviceFormView(generic.edit.FormView):
"""
RegisterDeviceFormView shows the form for registering new devices.
"""
template_name = "repapp/register_device.html"
form_class = RegisterDevice
@ -187,8 +140,7 @@ class RegisterGuestFormView(generic.edit.FormView):
password=password)
user.save()
print(f'{mail} {password}')
# TODO: send account creation email
send_guest_account_mail(guest, password, self.request)
send_confirmation_mails(device, guest, cafe, self.request)
@ -236,7 +188,8 @@ def device_view(request, device_identifier):
if not device.guest:
raise PermissionDenied()
# TODO: Guests can only view their devices, Reparateur and Orga can view all devices.
if not (is_member(user) or device.guest.mail == user.email):
raise PermissionDenied()
return render(
request,
@ -267,3 +220,38 @@ def member_login(request):
request,
"repapp/member_login.html"
)
def cron(request):
pass
def process_mails(request):
pass
def one_time_login(request, secret):
time.sleep(1)
otl = get_object_or_404(OneTimeLogin, secret=secret)
if otl.login_used:
messages.add_message(request, messages.ERROR,
'Der Einmal-Login wurde schon verwendet und ist nichtmehr gültig.')
new_otl = create_one_time_login(otl.user, otl.url)
send_one_time_login_mail(new_otl, request)
return HttpResponseRedirect(reverse_lazy('index'))
else:
otl.login_used = True
otl.login_date = now()
otl.save()
user = authenticate(request, username=secret, password=None)
if user is not None:
login(request, user)
messages.add_message(request, messages.INFO, 'Login erfolgreich!')
return HttpResponseRedirect(otl.url)
else:
messages.add_message(request, messages.ERROR, 'Login fehlgeschlagen.')
return HttpResponseRedirect(reverse_lazy('index'))

View file

@ -1,4 +0,0 @@
from django.contrib import admin
from .models import CustomUser
admin.site.register(CustomUser)

View file

@ -1,6 +0,0 @@
from django.apps import AppConfig
class RepappUsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'repapp_users'

View file

@ -1,65 +0,0 @@
import unicodedata
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
from repapp.models import Organisator, Reparateur
class EmailBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=username)
except UserModel.DoesNotExist:
return None
else:
if user.check_password(password):
return user
return None
def generate_username(email):
# Using Python 3 and Django 1.11+, usernames can contain alphanumeric
# (ascii and unicode), _, @, +, . and - characters. So we normalize
# it and slice at 150 characters.
return unicodedata.normalize('NFKC', email)[:150]
def create_repapp_user(user):
organisator = Organisator.objects.filter(mail=user.email).first()
if not organisator:
reparateur = Reparateur.objects.filter(mail=user.email).first()
if not reparateur:
reparateur = Reparateur(
name=user.username,
mail=user.email,
)
reparateur.save()
else:
reparateur.name = user.username
reparateur.save()
else:
organisator.name = user.username
organisator.save()
class KeycloakOIDCAB(OIDCAuthenticationBackend):
def create_user(self, claims):
user = super(KeycloakOIDCAB, self).create_user(claims)
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.save()
create_repapp_user(user)
return user
def update_user(self, user, claims):
user.username = claims.get(
'preferred_username', generate_username(user.email))
user.save()
create_repapp_user(user)
return user

View file

@ -1,44 +0,0 @@
# Generated by Django 4.2 on 2023-04-23 10:29
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
('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()),
],
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 4.2 on 2023-04-23 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('repapp_users', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='customuser',
options={'verbose_name': 'Benutzer', 'verbose_name_plural': 'Benutzer'},
),
migrations.AlterField(
model_name='customuser',
name='email',
field=models.EmailField(max_length=254, unique=True, verbose_name='eMail Adresse'),
),
]

View file

@ -1,14 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils.translation import gettext_lazy as _
class CustomUser(AbstractUser):
email = models.EmailField(unique=True, verbose_name=_("eMail Adresse"))
class Meta:
verbose_name = _('Benutzer')
verbose_name_plural = _('Benutzer')
def __str__(self):
return f'{self.email}'

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.