from __future__ import annotations import uuid from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.contrib.postgres.fields import CIEmailField from django.core.mail import EmailMessage, get_connection from django.db import models from django.template.loader import get_template from django.utils import timezone from django_prometheus.models import ExportModelOperationsMixin from desecapi import logger, metrics class MyUserManager(BaseUserManager): def create_user(self, email, password, **extra_fields): """ Creates and saves a User with the given email and password. """ if not email: raise ValueError('Users must have an email address') email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) user.save(using=self._db) return user class User(ExportModelOperationsMixin('User'), AbstractBaseUser): @staticmethod def _limit_domains_default(): return settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = CIEmailField( verbose_name='email address', unique=True, ) email_verified = models.DateTimeField(null=True, blank=True) is_active = models.BooleanField(default=True, null=True) is_admin = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) credentials_changed = models.DateTimeField(auto_now_add=True) limit_domains = models.PositiveIntegerField(default=_limit_domains_default.__func__, null=True, blank=True) needs_captcha = models.BooleanField(default=True) outreach_preference = models.BooleanField(default=True) objects = MyUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] def get_full_name(self): return self.email def get_short_name(self): return self.email def __str__(self): return self.email # noinspection PyMethodMayBeStatic def has_perm(self, *_): """Does the user have a specific permission?""" # Simplest possible answer: Yes, always return True # noinspection PyMethodMayBeStatic def has_module_perms(self, *_): """Does the user have permissions to view the app `app_label`?""" # Simplest possible answer: Yes, always return True @property def is_staff(self): """Is the user a member of staff?""" # Simplest possible answer: All admins are staff return self.is_admin def activate(self): self.is_active = True self.needs_captcha = False self.save() def change_email(self, email): old_email = self.email self.email = email self.credentials_changed = timezone.now() self.validate_unique() self.save() self.send_email('change-email-confirmation-old-email', recipient=old_email) def change_password(self, raw_password): self.set_password(raw_password) self.credentials_changed = timezone.now() self.save() self.send_email('password-change-confirmation') def delete(self): pk = self.pk ret = super().delete() logger.warning(f'User {pk} deleted') return ret def send_email(self, reason, context=None, recipient=None, subject=None, template=None): fast_lane = 'email_fast_lane' slow_lane = 'email_slow_lane' immediate_lane = 'email_immediate_lane' lanes = { 'activate-account': slow_lane, 'change-email': slow_lane, 'change-email-confirmation-old-email': fast_lane, 'change-outreach-preference': slow_lane, 'confirm-account': slow_lane, 'password-change-confirmation': fast_lane, 'reset-password': fast_lane, 'delete-account': fast_lane, 'domain-dyndns': fast_lane, 'renew-domain': immediate_lane, } if reason not in lanes: raise ValueError(f'Cannot send email to user {self.pk} without a good reason: {reason}') context = context or {} template = template or get_template(f'emails/{reason}/content.txt') content = template.render(context) content += f'\nSupport Reference: user_id = {self.pk}\n' logger.warning(f'Queuing email for user account {self.pk} (reason: {reason}, lane: {lanes[reason]})') num_queued = EmailMessage( subject=(subject or get_template(f'emails/{reason}/subject.txt').render(context)).strip(), body=content, from_email=get_template('emails/from.txt').render(), to=[recipient or self.email], connection=get_connection(lane=lanes[reason], debug={'user': self.pk, 'reason': reason}) ).send() metrics.get('desecapi_messages_queued').labels(reason, self.pk, lanes[reason]).observe(num_queued) return num_queued