""" Views of RepApp. """ import datetime import random import string import time import os import logging 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 django.views import generic from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse from django.core.exceptions import PermissionDenied from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth import authenticate, login from django.utils.timezone import now from .forms import RegisterDevice, RegisterGuest from .models import (Cafe, Device, Guest, OneTimeLogin, CustomUser, Organisator, Reparateur) from . import mail_interface logger = logging.getLogger(__name__) 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( '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, }) 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, 'Sie haben einen neuen Login per eMail erhalten.') def send_confirmation_mails(device, guest, cafe, request): """ Send confirmation mails for device registration. """ 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) secret = create_one_time_login(guest.user, url) login_url = request.build_absolute_uri(f'/onetimelogin/{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): """ Send guest user account created mail. """ 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, }) send_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 send_ok > 0: messages.add_message(request, messages.INFO, 'Sie haben ihre Benutzerdaten per eMail erhalten.') def is_member(user): """ Test is a user is a Repair-Café member. """ organisator = Organisator.objects.filter(mail=user.email).first() reparateur = Reparateur.objects.filter(mail=user.email).first() return organisator or reparateur def is_organisator(user): """ Test is a user is a Repair-Café organisator. """ organisator = Organisator.objects.filter(mail=user.email).first() return organisator is not None def is_reparateur(user): """ Test is a user is a Repair-Café reparateur. """ reparateur = Reparateur.objects.filter(mail=user.email).first() return reparateur is not None 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 class IndexView(generic.ListView): """ The IndexView lists all future Repair-Cafés. """ template_name = "repapp/index.html" def get_queryset(self): messages.add_message(self.request, messages.INFO, 'Dies ist eine Test. Leider können sie sich hier noch nicht anmelden.') return Cafe.objects.filter(event_date__gte=datetime.date.today()) class RegisterDeviceFormView(generic.edit.FormView): """ RegisterDeviceFormView shows the form for registering new devices. """ template_name = "repapp/register_device.html" form_class = RegisterDevice def form_valid(self, form): cafe = self.kwargs['cafe'] cafe = get_object_or_404(Cafe, pk=cafe) mail = form.cleaned_data['mail'] device = form.cleaned_data['device'] identifier = sha256( f'{device}{mail}{datetime.datetime.now()}'.encode('utf-8') ).hexdigest() device = Device( identifier=identifier, device=device, manufacturer=form.cleaned_data['manufacturer'], error=form.cleaned_data['error'], follow_up=form.cleaned_data['follow_up'], device_picture=self.request.FILES.get("device_picture", None), type_plate_picture=self.request.FILES.get( "type_plate_picture", None), cafe=cafe, confirmed=False, ) device.save() guest = Guest.objects.filter(mail=mail).first() user = CustomUser.objects.filter(email=mail).first() if guest and user: device.guest = guest device.save() guest.user = user guest.save() send_confirmation_mails(device, guest, cafe, self.request) resp = HttpResponseRedirect( reverse_lazy('register_device_final', kwargs={ 'cafe': cafe.pk, 'device_identifier': device.identifier}) ) return resp else: return HttpResponseRedirect( reverse_lazy('register_guest', kwargs={ 'cafe': cafe.pk, 'device_identifier': device.identifier, 'mail': mail}) ) def get_context_data(self, **kwargs): cafe = self.kwargs['cafe'] cafe = get_object_or_404(Cafe, pk=cafe) context = super(RegisterDeviceFormView, self).get_context_data( **kwargs ) context["cafe"] = cafe return context class RegisterGuestFormView(generic.edit.FormView): """ View for registering a new guest. """ template_name = "repapp/register_guest.html" form_class = RegisterGuest def form_valid(self, form): cafe = self.kwargs['cafe'] cafe = get_object_or_404(Cafe, pk=cafe) device_identifier = self.kwargs['device_identifier'] device = get_object_or_404(Device, identifier=device_identifier) name = form.cleaned_data['name'] mail = self.kwargs['mail'] residence = form.cleaned_data['residence'] identifier = sha256( f'{name}{residence}{datetime.datetime.now()}'.encode('utf-8') ).hexdigest() guest = Guest( identifier=identifier, name=name, phone=form.cleaned_data['phone'], residence=residence, mail=mail, ) guest.save() device.guest = guest device.save() if not CustomUser.objects.filter(email=mail).exists(): password = ''.join(random.choice(string.ascii_letters) for i in range(10)) username = name i = 1 while CustomUser.objects.filter(username=username).first(): username = f'{name}{i}' i = i + 1 user = CustomUser.objects.create_user( username=username, email=mail, ) user.set_password(password) user.save() guest.user = user guest.save() send_guest_account_mail(guest, password, self.request) send_confirmation_mails(device, guest, cafe, self.request) return HttpResponseRedirect( reverse_lazy('register_device_final', kwargs={ 'cafe': cafe.pk, 'device_identifier': device.identifier}) ) def get_context_data(self, **kwargs): cafe = self.kwargs['cafe'] cafe = get_object_or_404(Cafe, pk=cafe) device_identifier = self.kwargs['device_identifier'] device = get_object_or_404(Device, identifier=device_identifier) mail = self.kwargs['mail'] guest = Guest.objects.filter(mail=mail).first() if guest: return HttpResponseRedirect( reverse_lazy('register_device_final', kwargs={ 'cafe': cafe.pk, 'device_identifier': device.identifier}) ) context = super(RegisterGuestFormView, self).get_context_data( **kwargs ) context["cafe"] = cafe context["device"] = device context["mail"] = mail return context def register_device_final(request, cafe, device_identifier): """ View to confirm device registration. """ cafe = get_object_or_404(Cafe, pk=cafe) device = get_object_or_404(Device, identifier=device_identifier) return render( request, "repapp/register_device_final.html", {"device": device, 'cafe': cafe}, ) @login_required def device_view(request, device_identifier): """ View for showing device details. """ user = request.user if not user: raise PermissionDenied() device = get_object_or_404(Device, identifier=device_identifier) if not device.guest: raise PermissionDenied() if not (is_member(user) or device.guest.mail == user.email): raise PermissionDenied() return render( request, "repapp/device_view.html", {"device": device}, ) @login_required def profile(request): """ View for showing guest details. """ user = request.user if not user: raise PermissionDenied() guest = get_object_or_404(Guest, mail=user.email) return render( request, "repapp/guest_profile.html", { 'guest': guest } ) def member(request): """ Login page for repair cafe members, using OIDC. """ # TODO: test logger.debug('Member-View: User: %s', str(request.user)) if request.user.is_anonymous or not is_member(request.user): return HttpResponseRedirect(reverse_lazy('oidc_authentication_init')) if is_organisator(request.user) and is_reparateur(request.user): return HttpResponseRedirect(reverse_lazy('select_role')) if is_organisator(request.user): return HttpResponseRedirect(reverse_lazy('orga')) if is_reparateur(request.user): return HttpResponseRedirect(reverse_lazy('repa')) logger.warning('User %s is no member!', str(request.user)) raise PermissionDenied('No member!') @login_required(login_url=reverse_lazy('member')) def orga(request): """ Landing page for organisators. """ # TODO: test if not is_organisator(request.user): logger.warning('The user %s is no organisator!', str(request.user)) raise PermissionDenied('Not organisator!') return render( request, "repapp/orga/main.html" ) @login_required(login_url=reverse_lazy('member')) def select_role(request): """ Landing page for organisators. """ # TODO: test if not is_organisator(request.user): logger.warning('The user %s is no organisator!', str(request.user)) raise PermissionDenied('Not organisator!') if not is_reparateur(request.user): logger.warning('The user %s is no reparateur!', str(request.user)) raise PermissionDenied('Not reparateur!') return render( request, "repapp/orga/select_role.html" ) @login_required(login_url=reverse_lazy('member')) def repa(request): """ Organisator main menu. """ # TODO: test if not is_reparateur(request.user): logger.warning('The user %s is no reparateur!', str(request.user)) raise PermissionDenied('Not reparateur!') return render( request, "repapp/repa/main.html" ) class CafeView(LoginRequiredMixin, generic.ListView): """ List of all Repair-Cafés. """ # TODO: test login_url = reverse_lazy('member') template_name = "repapp/orga/cafe.html" def get_queryset(self): return Cafe.objects.all() def cron(request): """ View to trigger automated regular tasks. """ return HttpResponse('done.') def process_mails(request): """ View to trigger processing of email in inbox. """ count = mail_interface.process_mails() return HttpResponse(f'Done. Processed {count} messages.') def one_time_login(request, secret: str): """ View for one time login. """ # waste a little time as brute force protection time.sleep(1) secret_hash = sha256(secret.encode('utf-8')).hexdigest() otl = get_object_or_404(OneTimeLogin, secret=secret_hash) if otl.login_used: messages.add_message(request, messages.ERROR, 'Der Einmal-Login wurde schon verwendet und ist nichtmehr gültig.') new_secret = create_one_time_login(otl.user, otl.url) send_one_time_login_mail(new_secret, otl.user.email, request) return HttpResponseRedirect(reverse_lazy('index')) else: otl.login_used = True otl.login_date = now() otl.save() user = authenticate(request, username=secret_hash, 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')) def bootstrap(request): """ Bootstrap design test page. """ return render( request, "repapp/bootstrap.html" ) class CafeCreateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.CreateView): """ Organisator view for creating a new Repair-Café. """ # TODO: create test login_url = reverse_lazy('member') template_name = "repapp/orga/create_cafe.html" model = Cafe fields = ['event_date', 'location', 'address'] success_url = reverse_lazy('cafe') def test_func(self): return is_organisator(self.request.user) class CafeUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.UpdateView): """ Organisator view for edit a Repair-Café. """ # TODO: create test login_url = reverse_lazy('member') template_name = "repapp/orga/edit_cafe.html" model = Cafe fields = ['event_date', 'location', 'address'] success_url = reverse_lazy('cafe') def test_func(self): logger.debug('User passes test: user: %s, result: %s.', self.request.user, is_organisator(self.request.user)) return is_organisator(self.request.user) class CafeDeleteView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.DeleteView): """ Organisator view for edit a Repair-Café. """ # TODO: create test login_url = reverse_lazy('member') template_name = "repapp/orga/delete_cafe.html" model = Cafe success_url = reverse_lazy('cafe') def test_func(self): logger.debug('User passes test: user: %s, result: %s.', self.request.user, is_organisator(self.request.user)) return is_organisator(self.request.user) class GuestView(LoginRequiredMixin, generic.ListView): """ List of all Repair-Cafés. """ # TODO: test login_url = reverse_lazy('member') template_name = "repapp/orga/guest.html" def get_queryset(self): return Guest.objects.all() class GuestUpdateView(LoginRequiredMixin, UserPassesTestMixin, generic.edit.UpdateView): """ Organisator view for edit a guest. """ # TODO: create test login_url = reverse_lazy('member') template_name = "repapp/orga/edit_guest.html" model = Guest success_url = reverse_lazy('guest') def test_func(self): return is_organisator(self.request.user)