# -*- encoding: utf-8 -*- import hashlib from datetime import datetime import logging import math from django.contrib.auth import get_user_model from django.utils.crypto import get_random_string from django.core.mail import send_mail from django.core.validators import validate_email from django.core.exceptions import ValidationError from django.conf import settings as appSettings from . import models # Get an instance of a logger logger = logging.getLogger(__name__) # Our user model USER_MODEL = get_user_model() def send_mail_safe(subject, message, from_email, recipient_list): """ Posle mail. Vyskytne-li se pri odesilani chyba, vypise ji do logu. Vrati uspesnost. """ try: send_mail(subject, message, from_email, recipient_list) except Exception as e: logger.error("send_mail failed with error: %s" % str(e)) return False return True class EmailTokenAuthBackend: """ Provides authorization via email workflow. The user is verified by a token previously sent by email. Zde se take vytvari skutecny uzivatel (model AppUser) z registrovaneho (model AppRegEmail) """ def get_user(self, user_id): try: return USER_MODEL.objects.get(pk=user_id) except USER_MODEL.DoesNotExist: return None def authenticate(self, request, emailToken=None): if emailToken is None: return None # do we have a user with this valid token ? try: u = USER_MODEL.objects.get(emailToken=emailToken) if u.etStamp and (datetime.now() - u.etStamp).total_seconds() < int(appSettings.TOKEN_VALID_SEC): u.emailToken = None u.etStamp = None u.save() return u else: u.emailToken = None u.etStamp = None u.save() raise ValidationError("Přihlašovací odkaz vypršel, nechte si poslat nový.") except USER_MODEL.DoesNotExist: pass # is this a first-time login, so the user has to be created ? rslt = models.AppRegEmail.objects.filter(emailToken=emailToken) if len(rslt) == 0: return None elif len(rslt) == 1: reg = rslt[0] if reg.etStamp and (datetime.now() - reg.etStamp).total_seconds() < int(appSettings.TOKEN_VALID_SEC): rsltUsers = models.AppUser.objects.filter(username=reg.email) if len(rsltUsers) != 0 : raise ValidationError("Uživatelské jméno je již obsazeno.") rsltUsers = models.AppUser.objects.filter(email=reg.email) if len(rsltUsers) != 0 : raise ValidationError("E-mailová adresa je již obsazena.") # token valid, create the user u = USER_MODEL() u.username = reg.email u.email = reg.email u.phone = reg.phone u.postcode = reg.postcode u.kind = reg.kind u.interestedIn = reg.interestedIn u.userform = reg.userform u.dc_stamp = datetime.now() u.save() # remove the approved email from registration reg.userform = None reg.save() reg.delete() return u else: raise ValidationError("Registrační odkaz vypršel, nechte si poslat nový.") else: # multiple tokens, which should not happen raise Exception("Multiple records with the same token.") return None def sendLoginToken(user): """ Generate and send a token to the user. """ emailToken = get_random_string(120) user.emailToken = emailToken user.etStamp = datetime.now() user.save() emailBody = """\ Dobrý den, níže zasíláme přihlašovací odkaz do aplikace Pirátů "Nalodění": {baseUrl}/prihlaseni/?t={emailToken} Přihlásíte se kliknutím na odkaz, nebo jeho překopírováním do prohlížeče internetových stránek. Odkaz je možné použít pouze jednou, v případě potřeby si nechte zaslat nový odkaz. S pozdravem Piráti """ send_mail_safe( 'Piráti - nalodění - přihlašovací odkaz', emailBody.format( emailToken=emailToken, baseUrl=appSettings.BASE_URL), "nalodeni@pirati.cz", [user.email] ) def sendRegisterToken(f_email): """ Generate and send a token to the registered email. """ # Check for user non-existence with this email f_email = f_email.strip().lower() rslt = models.AppUser.objects.filter(email__iexact=f_email) if len(rslt) == 0: pass elif len(rslt) == 1: # send login token instead return sendLoginToken(rslt[0]) else: logger.error("More AppUser objects with the same email.") return # check registrations rslt = models.AppRegEmail.objects.filter(email__iexact=f_email) if len(rslt) == 1: rt = rslt[0] elif len(rslt) == 0: rt = models.AppRegEmail() rt.email = f_email else: logger.error("More AppRegEmail objects with the same email.") return sendRegisterTokenReg(rt) def sendRegisterTokenReg(rt): """ Send registration email to an already existing AppRegEmail instance 'rt'. """ # create token emailToken = get_random_string(120) rt.emailToken = emailToken rt.etStamp = datetime.now() rt.save() # TODO: tohle je jen QuickFix. Validovat zde settings je dost pozde, # Melo by spadnout pri startu: predelat ten bordylek v main/settings.py # pote predelat vsude kde se pouziva konstrukce int(TOKEN_VALID_SEC) try: token_validity = int(appSettings.TOKEN_VALID_SEC) except: token_validity = 60*60*24*3 logger.error("invalid seting NALODENI_TOKEN_VALID_SEC - should be string") word_days = "dní" if token_validity < 60*60*24*5: word_days = "dny" if token_validity <= 60*60*24*1: word_days = "den" emailBody = """\ Vítej na palubě! Pro přihlášení do Nalodění klikni na následující odkaz: {baseUrl}/prihlaseni/?t={emailToken} Odkaz je možné použít pouze jednou, v případě potřeby si nech zaslat nový. Odkaz platí {days} {word}. S pozdravem Piráti """ send_mail_safe( 'Piráti - nalodění - registrační odkaz', emailBody.format( emailToken=emailToken, baseUrl=appSettings.BASE_URL, days=int(math.floor(token_validity / (60*60*24))), word=word_days), "nalodeni@pirati.cz", [rt.email] ) def sendEmailContactVerificationToken(user): """ Send validation email to user.email_contact. """ # The wanted new value, save it here so we do not accidentally change it, # or someone else through the user-object. ecw = user.email_contact if ecw is None or ecw == "": raise ValidationError("Chybí kontaktní email.") # ValidationError se odchytava vyse validate_email(ecw) # create token rand_str = get_random_string(120) user.email_contact_token = "%s-%s" % ( int(datetime.now().timestamp()), rand_str ) user.email_contact_verified = False user.save() sha256hash = hashlib.sha256() sha256hash.update(rand_str.encode('utf-8')) sha256hash.update(ecw.encode('utf-8')) emailToken = sha256hash.hexdigest() emailBody = """\ Dobrý den, níže Vám zasíláme odkaz pro ověření platnosti kontaktní e-mailové adresy. Změna adresy na {email_contact} byla vyžádána v aplikace Pirátů "Nalodění". Adresu ověříte přihlášením se do aplikace Nalodění (https://nalodeni.pirati.cz/prihlaseni/) a následným kliknutím na odkaz: {baseUrl}/ja-pirat/profil/?t={emailToken} Pokud jste tuto změnu nežádali, můžete tuto zprávu smazat. S pozdravem Piráti """ send_mail_safe( 'Piráti - nalodění - ověření kontaktního emailu', emailBody.format( email_contact = ecw, emailToken=emailToken, baseUrl=appSettings.BASE_URL), "nalodeni@pirati.cz", [ecw] ) return True #TODO:: osetreni transakci (pro pripad utoku typu DDoS)