Too many things to remember all of them
Finally got the mail queue working Oh yeah, queueing Added a TOTP provider which doesn't do anything much yet Probably a hell of a lot of other things that I just can't remember
This commit is contained in:
parent
bd0519146c
commit
d549897186
20 changed files with 171 additions and 14 deletions
|
@ -21,15 +21,16 @@ class BaseMailProvider:
|
||||||
def send_mail(self, subject, content, recipients, cc=[], bcc=[], headers={}, sender=getValue("core.mail.sender", "expephalon@localhost")):
|
def send_mail(self, subject, content, recipients, cc=[], bcc=[], headers={}, sender=getValue("core.mail.sender", "expephalon@localhost")):
|
||||||
message = email.message_from_string(content)
|
message = email.message_from_string(content)
|
||||||
headers["From"] = sender
|
headers["From"] = sender
|
||||||
headers["To"] = ",".join(recipients)
|
headers["To"] = recipients if type(recipients) == str else ",".join(recipients)
|
||||||
headers["Cc"] = ",".join(cc)
|
headers["Cc"] = cc if type(cc) == str else ",".join(cc)
|
||||||
headers["Bcc"] = ",".join(bcc)
|
headers["Bcc"] = bcc if type(bcc) == str else ",".join(bcc)
|
||||||
headers["Subject"] = subject
|
headers["Subject"] = subject
|
||||||
headers["Message-ID"] = email.utils.make_msgid("expephalon", urlparse(getValue("core.base_url", "http://localhost/").split(":")[1]).netloc)
|
headers["Message-ID"] = email.utils.make_msgid("expephalon", urlparse(getValue("core.base_url", "http://localhost/").split(":")[1]).netloc)
|
||||||
headers["Date"] = email.utils.formatdate()
|
headers["Date"] = email.utils.formatdate()
|
||||||
for header, value in headers.items():
|
for header, value in headers.items():
|
||||||
message.add_header(header, value)
|
if value:
|
||||||
self.send_message(message)
|
message.add_header(header, value)
|
||||||
|
return self.send_message(message)
|
||||||
|
|
||||||
class SMTPMailProvider(BaseMailProvider):
|
class SMTPMailProvider(BaseMailProvider):
|
||||||
def __init__(self, host=getValue("core.smtp.host", "localhost"), port=int(getValue("core.smtp.port", 0)), username=getValue("core.smtp.username", "") or None, password=getValue("core.smtp.password", "")):
|
def __init__(self, host=getValue("core.smtp.host", "localhost"), port=int(getValue("core.smtp.port", 0)), username=getValue("core.smtp.username", "") or None, password=getValue("core.smtp.password", "")):
|
||||||
|
@ -42,4 +43,4 @@ class SMTPMailProvider(BaseMailProvider):
|
||||||
return "SMTP Mail"
|
return "SMTP Mail"
|
||||||
|
|
||||||
def send_message(self, message):
|
def send_message(self, message):
|
||||||
self.smtp.send_message(message)
|
return self.smtp.send_message(message)
|
||||||
|
|
|
@ -18,3 +18,6 @@ class OTPVerificationForm(Form):
|
||||||
class PWResetForm(Form):
|
class PWResetForm(Form):
|
||||||
password1 = CharField(widget=PasswordInput)
|
password1 = CharField(widget=PasswordInput)
|
||||||
password2 = CharField(widget=PasswordInput)
|
password2 = CharField(widget=PasswordInput)
|
||||||
|
|
||||||
|
class PWRequestForm(Form):
|
||||||
|
email = EmailField()
|
2
core/helpers/auth.py
Normal file
2
core/helpers/auth.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
def generate_pwreset_mail(user, token):
|
||||||
|
pass
|
|
@ -1,4 +1,5 @@
|
||||||
from core.modules.mail import providers
|
from core.modules.mail import providers
|
||||||
|
from core.tasks.mail import send_mail as send_mail_task
|
||||||
|
|
||||||
from dbsettings.functions import getValue
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
@ -8,5 +9,12 @@ def get_provider_by_name(name, fallback=True):
|
||||||
def get_default_provider(fallback=True):
|
def get_default_provider(fallback=True):
|
||||||
return get_provider_by_name(getValue("core.email.provider", "smtp"), fallback)
|
return get_provider_by_name(getValue("core.email.provider", "smtp"), fallback)
|
||||||
|
|
||||||
def send_mail(provider=None, *args):
|
def send_mail(provider=get_default_provider(), **kwargs):
|
||||||
return get_provider_by_name(provider)().mail(*args)
|
provider = get_provider_by_name(provider) if type(provider) == str else provider
|
||||||
|
return send_mail_task.delay(provider, **kwargs)
|
||||||
|
|
||||||
|
def simple_send_mail(subject, content, recipients, cc=[], bcc=[], headers={}):
|
||||||
|
return send_mail(subject=subject, content=content, recipients=recipients, cc=cc, bcc=bcc, headers=headers)
|
||||||
|
|
||||||
|
def fetch_templates(template_name):
|
||||||
|
pass
|
1
core/tasks/__init__.py
Normal file
1
core/tasks/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from core.tasks.mail import *
|
5
core/tasks/mail.py
Normal file
5
core/tasks/mail.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from celery import shared_task
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def send_mail(provider=None, **kwargs):
|
||||||
|
return provider().send_mail(**kwargs)
|
|
@ -1,14 +1,16 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.views.generic import FormView, View
|
from django.views.generic import FormView, View
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout, get_user_model
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm
|
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm
|
||||||
from core.models.auth import LoginSession, PWResetToken
|
from core.models.auth import LoginSession, PWResetToken
|
||||||
from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name
|
from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name
|
||||||
|
from core.helpers.mail import send_mail
|
||||||
|
from core.helpers.auth import generate_pwreset_mail
|
||||||
|
|
||||||
from dbsettings.functions import getValue
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
@ -153,3 +155,16 @@ class PWResetView(FormView):
|
||||||
user.save()
|
user.save()
|
||||||
messages.success(self.request, "Your password has been changed. You can now login with your new password.")
|
messages.success(self.request, "Your password has been changed. You can now login with your new password.")
|
||||||
return redirect("login")
|
return redirect("login")
|
||||||
|
|
||||||
|
class PWRequestView(FormView):
|
||||||
|
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/pwrequest.html"
|
||||||
|
form_class = PWRequestForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
user = get_user_model().objects.get(username=form.cleaned_data["email"])
|
||||||
|
token = PWResetToken.objects.create(user=user)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.")
|
||||||
|
return redirect("login")
|
2
debian-packages.txt
Normal file
2
debian-packages.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rabbitmq-server
|
||||||
|
memcached
|
|
@ -0,0 +1,3 @@
|
||||||
|
from expephalon.celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ('celery_app',)
|
22
expephalon/celery.py
Normal file
22
expephalon/celery.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# set the default Django settings module for the 'celery' program.
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'expephalon.settings')
|
||||||
|
|
||||||
|
app = Celery('expephalon')
|
||||||
|
|
||||||
|
# Using a string here means the worker doesn't have to serialize
|
||||||
|
# the configuration object to child processes.
|
||||||
|
# - namespace='CELERY' means all celery-related configuration keys
|
||||||
|
# should have a `CELERY_` prefix.
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
|
||||||
|
# Load task modules from all registered Django app configs.
|
||||||
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
|
|
||||||
|
@app.task(bind=True)
|
||||||
|
def debug_task(self):
|
||||||
|
print('Request: {0!r}'.format(self.request))
|
|
@ -29,7 +29,8 @@ DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
# To add frontend or backend templates, move them to the /templates subdirectory, then insert their name (i.e. the directory name) in the appropriate field. Move any required statics to the /static subdirectory.
|
# To add frontend or backend templates, move them to the /templates subdirectory, then insert their name (i.e. the directory name) in the
|
||||||
|
# appropriate field. Move any required statics to the /static subdirectory
|
||||||
|
|
||||||
EXPEPHALON_FRONTEND = "frontend"
|
EXPEPHALON_FRONTEND = "frontend"
|
||||||
EXPEPHALON_BACKEND = "backend"
|
EXPEPHALON_BACKEND = "backend"
|
||||||
|
@ -37,3 +38,15 @@ EXPEPHALON_BACKEND = "backend"
|
||||||
# To add Expephalon modules, move them to the Expephalon root directory, then add them to this list
|
# To add Expephalon modules, move them to the Expephalon root directory, then add them to this list
|
||||||
|
|
||||||
EXPEPHALON_MODULES = []
|
EXPEPHALON_MODULES = []
|
||||||
|
|
||||||
|
# To use memcached for caching, add IP:PORT or unix:PATH - default setting should be good for an unmodified local setup of memcached
|
||||||
|
|
||||||
|
MEMCACHED_LOCATION = ["127.0.0.1:11211"]
|
||||||
|
|
||||||
|
# RabbitMQ is required for queues to work - default settings should be good for an unmodified local setup of RabbitMQ,
|
||||||
|
# but you might still want to configure it to use a password
|
||||||
|
|
||||||
|
RABBITMQ_LOCATION = "127.0.0.1:5672"
|
||||||
|
RABBITMQ_VHOST = ""
|
||||||
|
RABBITMQ_USER = "guest"
|
||||||
|
RABBITMQ_PASS = "guest"
|
|
@ -18,6 +18,8 @@ INSTALLED_APPS = [
|
||||||
'bootstrap4',
|
'bootstrap4',
|
||||||
'core',
|
'core',
|
||||||
'dbsettings',
|
'dbsettings',
|
||||||
|
'django_celery_results',
|
||||||
|
'django_celery_beat',
|
||||||
] + EXPEPHALON_MODULES
|
] + EXPEPHALON_MODULES
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -123,3 +125,22 @@ PASSWORD_HASHERS = [
|
||||||
|
|
||||||
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
|
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
|
||||||
AWS_DEFAULT_ACL = None
|
AWS_DEFAULT_ACL = None
|
||||||
|
|
||||||
|
# Caching
|
||||||
|
|
||||||
|
if MEMCACHED_LOCATION:
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||||
|
'LOCATION': MEMCACHED_LOCATION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Queue
|
||||||
|
|
||||||
|
CELERY_TASK_SERIALIZER = "pickle"
|
||||||
|
CELERY_RESULT_SERIALIZER = "pickle"
|
||||||
|
CELERY_ACCEPT_CONTENT = ['pickle']
|
||||||
|
CELERY_CACHE_BACKEND = 'default'
|
||||||
|
CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_LOCATION}/{RABBITMQ_VHOST}"
|
||||||
|
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
0
mail_templates/pwreset_mail.html
Normal file
0
mail_templates/pwreset_mail.html
Normal file
11
mail_templates/pwreset_mail.txt
Normal file
11
mail_templates/pwreset_mail.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Hi {{ first_name }},
|
||||||
|
|
||||||
|
Somebody (hopefully you) requested a new password for your {{ sitename }} account. If this was you, please click the following link to reset your password:
|
||||||
|
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
If it was not you, you can ignore this message. The link will expire in 24 hours.
|
||||||
|
|
||||||
|
Best regards
|
||||||
|
|
||||||
|
Your {{ sitename }} Team
|
|
@ -8,3 +8,6 @@ django-phonenumber-field[phonenumbers]
|
||||||
django-bootstrap4
|
django-bootstrap4
|
||||||
wheel
|
wheel
|
||||||
git+https://kumig.it/kumisystems/django-dbsettings.git
|
git+https://kumig.it/kumisystems/django-dbsettings.git
|
||||||
|
celery
|
||||||
|
django-celery-results
|
||||||
|
django-celery-beat
|
||||||
|
|
0
totp/__init__.py
Normal file
0
totp/__init__.py
Normal file
5
totp/apps.py
Normal file
5
totp/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TotpConfig(AppConfig):
|
||||||
|
name = 'totp'
|
12
totp/models.py
Normal file
12
totp/models.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django.db.models import Model, CharField, ForeignKey, CASCADE, DateTimeField
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class TOTPUser(Model):
|
||||||
|
secret = CharField(max_length=32, default=pyotp.random_base32())
|
||||||
|
user = ForeignKey(get_user_model(), CASCADE)
|
29
totp/otp.py
Normal file
29
totp/otp.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from core.classes.otp import BaseOTPProvider
|
||||||
|
from totp.models import TOTPUser
|
||||||
|
|
||||||
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
import pyotp
|
||||||
|
|
||||||
|
class TOTP(BaseOTPProvider):
|
||||||
|
@property
|
||||||
|
def get_name(self):
|
||||||
|
return "Time-based OTP"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def start_authentication(self, user):
|
||||||
|
return "Please enter the token displayed in your app."
|
||||||
|
|
||||||
|
def validate_token(self, user, token):
|
||||||
|
try:
|
||||||
|
otpuser = TOTPUser.objects.get(user=user)
|
||||||
|
return pyotp.TOTP(otpuser.secret).verify(token)
|
||||||
|
except OTPUser.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
OTPPROVIDERS = {"totp": TOTP}
|
1
totp/requirements.txt
Normal file
1
totp/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pyotp
|
Loading…
Reference in a new issue