Integrate basic mail functionality (might need a queue...)

This commit is contained in:
Kumi 2020-04-18 15:02:41 +02:00
parent 53780751d1
commit bd0519146c
28 changed files with 273 additions and 42 deletions

0
chat/__init__.py Normal file
View file

3
chat/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
chat/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
name = 'chat'

3
chat/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
chat/tests.py Normal file
View file

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

3
chat/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

45
core/classes/mail.py Normal file
View file

@ -0,0 +1,45 @@
import smtplib
import email
import email.utils
from urllib.parse import urlparse
from dbsettings.functions import getValue
class BaseMailProvider:
@property
def get_name(self):
return "Base Mail Provider"
@property
def get_logo(self):
return ""
def send_message(self, message):
raise NotImplementedError(f"{type(self)} does not implement send_message()!")
def send_mail(self, subject, content, recipients, cc=[], bcc=[], headers={}, sender=getValue("core.mail.sender", "expephalon@localhost")):
message = email.message_from_string(content)
headers["From"] = sender
headers["To"] = ",".join(recipients)
headers["Cc"] = ",".join(cc)
headers["Bcc"] = ",".join(bcc)
headers["Subject"] = subject
headers["Message-ID"] = email.utils.make_msgid("expephalon", urlparse(getValue("core.base_url", "http://localhost/").split(":")[1]).netloc)
headers["Date"] = email.utils.formatdate()
for header, value in headers.items():
message.add_header(header, value)
self.send_message(message)
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", "")):
self.smtp = smtplib.SMTP(host, port)
if username:
self.smtp.login(username, password)
@property
def get_name(self):
return "SMTP Mail"
def send_message(self, message):
self.smtp.send_message(message)

View file

@ -1 +1,2 @@
from core.forms.auth import *
from core.forms.profiles import *

View file

@ -14,3 +14,7 @@ class OTPSelectorForm(Form):
class OTPVerificationForm(Form):
token = CharField()
class PWResetForm(Form):
password1 = CharField(widget=PasswordInput)
password2 = CharField(widget=PasswordInput)

View file

@ -1,15 +1,19 @@
from django.forms import ModelForm, CharField, EmailField
from django.forms import ModelForm, CharField, BooleanField, ImageField
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from phonenumber_field.formfields import PhoneNumberField
from core.models import AdminProfile
class AdminEditForm(ModelForm):
#fields from User model that you want to edit
first_name = CharField(required=True, label=_('First Name'))
last_name = CharField(required=True, label=_('Last Name'))
email = EmailField(required=True, labels=_("Email Address"))
display_name = CharField(required=False, label=_('Internal Display Name'))
mobile = PhoneNumberField(required=False, label=_('Mobile Number'))
role = CharField(required=False, label=_("Role"))
image = ImageField(required=False, label=_("Image"))
remove_image = BooleanField(required=False, label=_("Remove image from profile?"))
class Meta:
model = AdminProfile
fields = ('first_name', 'last_name', "email", 'mobile', "role", "image")
model = get_user_model()
fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image", "remove_image")

View file

@ -1,5 +1,5 @@
import uuid
import os.path
def generate_storage_filename():
return uuid.uuid4()
def generate_storage_filename(*args, **kwargs):
return "uploads/" + str(uuid.uuid4())

12
core/helpers/mail.py Normal file
View file

@ -0,0 +1,12 @@
from core.modules.mail import providers
from dbsettings.functions import getValue
def get_provider_by_name(name, fallback=True):
return providers.get(name, None) or providers["smtp"]
def get_default_provider(fallback=True):
return get_provider_by_name(getValue("core.email.provider", "smtp"), fallback)
def send_mail(provider=None, *args):
return get_provider_by_name(provider)().mail(*args)

View file

@ -1,3 +1,3 @@
from core.models.files import *
from core.models.profiles import *
from core.models.otp import *
from core.models.auth import *

View file

@ -11,3 +11,8 @@ class LoginSession(Model):
uuid = UUIDField(default=uuid4, primary_key=True)
user = ForeignKey(get_user_model(), CASCADE)
creation = DateTimeField(auto_now_add=True)
class PWResetToken(Model):
token = UUIDField(default=uuid4, primary_key=True)
user = ForeignKey(get_user_model(), CASCADE)
creation = DateTimeField(auto_now_add=True)

View file

@ -4,10 +4,17 @@ from phonenumber_field.modelfields import PhoneNumberField
from django.db.models import OneToOneField, CASCADE, CharField, ImageField
from django.contrib.auth import get_user_model
from core.helpers.files import generate_storage_filename
class Profile(PolymorphicModel):
user = OneToOneField(get_user_model(), CASCADE)
mobile = PhoneNumberField(blank=True)
class AdminProfile(Profile):
role = CharField(max_length=64)
image = ImageField(null=True)
role = CharField(max_length=255)
image = ImageField(null=True, blank=True, upload_to=generate_storage_filename)
display_name = CharField("Internal Display Name", max_length=255, null=True, blank=True)
@property
def get_internal_name(self):
return self.display_name if self.display_name else self.user.get_full_name

15
core/modules/mail.py Normal file
View file

@ -0,0 +1,15 @@
from core.classes.mail import SMTPMailProvider
import importlib
from django.conf import settings
providers = { "smtp": SMTPMailProvider }
for module in settings.EXPEPHALON_MODULES:
try:
mom = importlib.import_module(f"{module}.mail")
for name, provider in mom.MAILPROVIDERS.items():
providers[name] = provider
except (AttributeError, ModuleNotFoundError):
continue

View file

@ -11,7 +11,7 @@ navigations = {
for module in ["core"] + settings.EXPEPHALON_MODULES:
try:
mon = importlib.import_module(f"{module}.navigations")
mon = importlib.import_module(f"{module}.navigation")
for name, nav in mon.NAVIGATIONS:
if name in navigations.keys:
raise ValueError(f"Error in {module}: Navigation of name {name} already exists!")

View file

@ -9,6 +9,7 @@ from core.views import (
OTPSelectorView,
LogoutView,
OTPValidatorView,
PWResetView,
BackendNotImplementedView,
AdminListView,
AdminDeleteView,
@ -28,6 +29,7 @@ URLPATTERNS.append(path('login/', LoginView.as_view(), name="login"))
URLPATTERNS.append(path('login/otp/select/', OTPSelectorView.as_view(), name="otpselector"))
URLPATTERNS.append(path('login/otp/validate/', OTPValidatorView.as_view(), name="otpvalidator"))
URLPATTERNS.append(path('logout/', LogoutView.as_view(), name="logout"))
URLPATTERNS.append(path('login/reset/', PWResetView.as_view(), name="pwreset"))
# Base Backend URLs

View file

@ -88,7 +88,7 @@ navigations["backend_main"].add_section(reports_section)
administration_section = NavSection("Administration", "")
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "backendni")
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins")
brand_administration_item = NavItem("Brands", "fa-code-branch", "backendni")
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni")
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni")

View file

@ -4,11 +4,14 @@ from django.contrib.auth import authenticate, login, logout
from django.shortcuts import redirect
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.utils import timezone
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm
from core.models.otp import LoginSession
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm
from core.models.auth import LoginSession, PWResetToken
from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name
from dbsettings.functions import getValue
class LoginView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/login.html"
form_class = LoginForm
@ -126,3 +129,27 @@ class LogoutView(View):
def get(self, request, *args, **kwargs):
logout(request)
return redirect("login")
class PWResetView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/pwreset.html"
form_class = PWResetForm
def validate_session(self):
try:
token = PWResetToken.objects.get(token=self.kwargs["pk"])
max_age = int(getValue("core.auth.pwreset.max_age", "86400"))
assert token.creation > timezone.now() - timezone.timedelta(seconds=max_age)
return token.user
except:
messages.error(self.request, "Incorrect or expired password reset link.")
raise PermissionDenied()
def form_valid(self, form):
user = self.validate_session()
if not form.cleaned_data["password1"] == form.cleaned_data["password2"]:
messages.error(self.request, "Entered passwords do not match - please try again.")
return self.form_invalid(form)
user.set_password(form.cleaned_data["password1"])
user.save()
messages.success(self.request, "Your password has been changed. You can now login with your new password.")
return redirect("login")

View file

@ -4,6 +4,7 @@ from django.urls import reverse_lazy
from django.contrib.auth import get_user_model
from core.models import AdminProfile
from core.forms import AdminEditForm
class AdminListView(ListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html"
@ -16,32 +17,65 @@ class AdminListView(ListView):
class AdminEditView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html"
model = get_user_model()
success_url = reverse_lazy("dbsettings")
fields = ["key", "value"]
form_class = AdminEditForm
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Setting"
context["title"] = "Edit Administrator"
return context
def get_initial(self):
initial = super().get_initial()
admin = get_user_model().objects.get(id=self.kwargs["pk"])
assert type(admin.profile) == AdminProfile
initial["first_name"] = admin.first_name
initial["last_name"] = admin.last_name
initial["email"] = admin.username
initial["mobile"] = admin.profile.mobile
initial["role"] = admin.profile.role
initial["display_name"] = admin.profile.display_name
return initial
def form_valid(self, form):
admin = get_user_model().objects.get(id=self.kwargs["pk"])
admin.first_name = form.cleaned_data["first_name"]
admin.last_name = form.cleaned_data["last_name"]
admin.username = form.cleaned_data["email"]
admin.email = form.cleaned_data["email"]
admin.profile.mobile = form.cleaned_data["mobile"]
admin.profile.role = form.cleaned_data["role"]
admin.profile.display_name = form.cleaned_data["display_name"]
if form.cleaned_data["image"] or form.cleaned_data["remove_image"]:
admin.profile.image = form.cleaned_data["image"]
admin.profile.save()
admin.save()
return super().form_valid(form)
class AdminDeleteView(DeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html"
model = get_user_model()
success_url = reverse_lazy("dbsettings")
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Administrator"
return context
def get_object(self, queryset=None):
admin = super().get_object(queryset=queryset)
assert type(admin.profile) == AdminProfile
return admin
class AdminCreateView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html"
model = get_user_model()
success_url = reverse_lazy("dbsettings")
success_url = reverse_lazy("admins")
fields = ["key", "value"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Setting"
context["title"] = "Create Administrator"
return context

View file

@ -0,0 +1,28 @@
{% extends "backend/auth/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="mx-auto app-login-box col-sm-12 col-md-10 col-lg-9">
<div class="app-logo"></div>
<h4 class="mb-0">
<span class="d-block">Welcome back,</span>
<span>Let us help you regain access to your account.</span></h4>
<!--<h6 class="mt-3">No account? <a href="javascript:void(0);" class="text-primary">Sign up now</a></h6>-->
{% bootstrap_messages %}
<div class="divider row"></div>
<div>
<form method="POST" class="">
{% csrf_token %}
<div class="form-row">
<div class="col-md-6">
<div class="position-relative form-group"><label for="exampleEmail" class="">Email</label><input name="email" id="exampleEmail" placeholder="Email here..." type="email" class="form-control"></div>
</div>
</div>
<div class="divider row"></div>
<div class="d-flex align-items-center">
<button class="btn btn-primary btn-lg">Start Reset</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends "backend/auth/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="mx-auto app-login-box col-sm-12 col-md-10 col-lg-9">
<div class="app-logo"></div>
<h4 class="mb-0">
<span class="d-block">Welcome back,</span>
<span>Please set your password.</span></h4>
<!--<h6 class="mt-3">No account? <a href="javascript:void(0);" class="text-primary">Sign up now</a></h6>-->
{% bootstrap_messages %}
<div class="divider row"></div>
<div>
<form method="POST" class="">
{% csrf_token %}
<div class="form-row">
<div class="col-md-6">
<div class="position-relative form-group"><label for="examplePassword" class="">Password</label><input name="password1" id="examplePassword" placeholder="Password here..." type="password"
class="form-control"></div>
</div>
<div class="col-md-6">
<div class="position-relative form-group"><label for="examplePassword" class="">Confirm Password</label><input name="password2" id="examplePassword" placeholder="Password here..." type="password"
class="form-control"></div>
</div>
</div>
<div class="divider row"></div>
<div class="d-flex align-items-center">
<button class="btn btn-primary btn-lg">Set Password</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -70,21 +70,21 @@
</div>
<ul class="header-menu nav">
<li class="nav-iftem">
<a href="javascript:void(0);" class="nav-link">
<a href="{% url "dashboard" %}" class="nav-link">
<i class="nav-link-icon fa fa-database"> </i>
Statistics
Administration
</a>
</li>
<li class="btn-group nav-item">
<a href="javascript:void(0);" class="nav-link">
<i class="nav-link-icon fa fa-edit"></i>
Projects
<a href="{% url "backendni" %}" class="nav-link">
<i class="nav-link-icon fa fa-comment-dots"></i>
Chat
</a>
</li>
<li class="dropdown nav-item">
<a href="javascript:void(0);" class="nav-link">
<a href="{% url "admins_edit" request.user.id %}" class="nav-link">
<i class="nav-link-icon fa fa-cog"></i>
Settings
User Settings
</a>
</li>
</ul> </div>
@ -99,10 +99,7 @@
<i class="fa fa-angle-down ml-2 opacity-8"></i>
</a>
<div tabindex="-1" role="menu" aria-hidden="true" class="dropdown-menu dropdown-menu-right">
<button type="button" tabindex="0" class="dropdown-item">User Account</button>
<button type="button" tabindex="0" class="dropdown-item">Settings</button>
<h6 tabindex="-1" class="dropdown-header">Header</h6>
<button type="button" tabindex="0" class="dropdown-item">Actions</button>
<a href="{% url "admins_edit" request.user.id %}" type="button" tabindex="0" class="dropdown-item">User Account</a>
<div tabindex="-1" class="dropdown-divider"></div>
<a href="{% url "logout" %}" type="button" tabindex="0" class="dropdown-item">Logout</a>
</div>
@ -110,7 +107,7 @@
</div>
<div class="widget-content-left ml-3 header-user-info">
<div class="widget-heading">
{{ request.user.get_full_name }}
{{ request.user.profile.get_internal_name }}
</div>
<div class="widget-subheading">
{{ request.user.profile.role }}

View file

@ -33,7 +33,7 @@
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}

View file

@ -27,7 +27,7 @@
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting {{ object.user.username }}
Deleting {{ object.get_full_name }}
</div>
</div>
<div class="card-body">
@ -35,7 +35,7 @@
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete {{ object.user.username }}? This will irrevocably delete their account and any associated information. You can also disable the user without deleting their data!
Are you sure you wish to delete {{ object.get_full_name }}? This will irrevocably delete their account and any associated information. You can also disable the user without deleting their data!
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save

View file

@ -52,7 +52,7 @@
<td>{{ admin.role }}</td>
<td>{{ admin.user.username }}</td>
<td>{{ admin.mobile }}</td>
<td><a href=""><i class="fas fa-edit" title="Edit Setting"></i></a> <a href=""><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Setting"></i></a></td>
<td><a href="{% url "admins_edit" admin.user.id %}"><i class="fas fa-edit" title="Edit Administrator"></i></a> <a href="{% url "admins_delete" admin.user.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Administrator"></i></a></td>
</tr>
{% endfor %}
</tbody>

View file

@ -33,7 +33,7 @@
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}