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.auth import *
from core.forms.profiles import *

View file

@ -13,4 +13,8 @@ class OTPSelectorForm(Form):
self.fields['provider'] = ChoiceField(choices=otp_choices) self.fields['provider'] = ChoiceField(choices=otp_choices)
class OTPVerificationForm(Form): class OTPVerificationForm(Form):
token = CharField() 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.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _ 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 from core.models import AdminProfile
class AdminEditForm(ModelForm): class AdminEditForm(ModelForm):
#fields from User model that you want to edit display_name = CharField(required=False, label=_('Internal Display Name'))
first_name = CharField(required=True, label=_('First Name')) mobile = PhoneNumberField(required=False, label=_('Mobile Number'))
last_name = CharField(required=True, label=_('Last Name')) role = CharField(required=False, label=_("Role"))
email = EmailField(required=True, labels=_("Email Address")) image = ImageField(required=False, label=_("Image"))
remove_image = BooleanField(required=False, label=_("Remove image from profile?"))
class Meta: class Meta:
model = AdminProfile model = get_user_model()
fields = ('first_name', 'last_name', "email", 'mobile', "role", "image") fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image", "remove_image")

View file

@ -1,5 +1,5 @@
import uuid import uuid
import os.path import os.path
def generate_storage_filename(): def generate_storage_filename(*args, **kwargs):
return uuid.uuid4() 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.files import *
from core.models.profiles import * from core.models.profiles import *
from core.models.otp import * from core.models.auth import *

View file

@ -10,4 +10,9 @@ class OTPUser(Model):
class LoginSession(Model): class LoginSession(Model):
uuid = UUIDField(default=uuid4, primary_key=True) uuid = UUIDField(default=uuid4, primary_key=True)
user = ForeignKey(get_user_model(), CASCADE) 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) 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.db.models import OneToOneField, CASCADE, CharField, ImageField
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from core.helpers.files import generate_storage_filename
class Profile(PolymorphicModel): class Profile(PolymorphicModel):
user = OneToOneField(get_user_model(), CASCADE) user = OneToOneField(get_user_model(), CASCADE)
mobile = PhoneNumberField(blank=True) mobile = PhoneNumberField(blank=True)
class AdminProfile(Profile): class AdminProfile(Profile):
role = CharField(max_length=64) role = CharField(max_length=255)
image = ImageField(null=True) 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: for module in ["core"] + settings.EXPEPHALON_MODULES:
try: try:
mon = importlib.import_module(f"{module}.navigations") mon = importlib.import_module(f"{module}.navigation")
for name, nav in mon.NAVIGATIONS: for name, nav in mon.NAVIGATIONS:
if name in navigations.keys: if name in navigations.keys:
raise ValueError(f"Error in {module}: Navigation of name {name} already exists!") raise ValueError(f"Error in {module}: Navigation of name {name} already exists!")

View file

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

View file

@ -88,7 +88,7 @@ navigations["backend_main"].add_section(reports_section)
administration_section = NavSection("Administration", "") 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") brand_administration_item = NavItem("Brands", "fa-code-branch", "backendni")
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni") sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni")
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "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.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 core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm
from core.models.otp import LoginSession 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 dbsettings.functions import getValue
class LoginView(FormView): class LoginView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/login.html" template_name = f"{settings.EXPEPHALON_BACKEND}/auth/login.html"
form_class = LoginForm form_class = LoginForm
@ -125,4 +128,28 @@ class OTPValidatorView(FormView):
class LogoutView(View): class LogoutView(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
logout(request) 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") return redirect("login")

View file

@ -4,6 +4,7 @@ from django.urls import reverse_lazy
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from core.models import AdminProfile from core.models import AdminProfile
from core.forms import AdminEditForm
class AdminListView(ListView): class AdminListView(ListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html" template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html"
@ -16,32 +17,65 @@ class AdminListView(ListView):
class AdminEditView(FormView): class AdminEditView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html" template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html"
model = get_user_model() form_class = AdminEditForm
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("admins")
fields = ["key", "value"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Edit Setting" context["title"] = "Edit Administrator"
return context 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): class AdminDeleteView(DeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html" template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html"
model = get_user_model() model = get_user_model()
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Delete Administrator" context["title"] = "Delete Administrator"
return context return context
def get_object(self, queryset=None):
admin = super().get_object(queryset=queryset)
assert type(admin.profile) == AdminProfile
return admin
class AdminCreateView(FormView): class AdminCreateView(FormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html" template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html"
model = get_user_model() model = get_user_model()
success_url = reverse_lazy("dbsettings") success_url = reverse_lazy("admins")
fields = ["key", "value"] fields = ["key", "value"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Create Setting" context["title"] = "Create Administrator"
return context 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> </div>
<ul class="header-menu nav"> <ul class="header-menu nav">
<li class="nav-iftem"> <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> <i class="nav-link-icon fa fa-database"> </i>
Statistics Administration
</a> </a>
</li> </li>
<li class="btn-group nav-item"> <li class="btn-group nav-item">
<a href="javascript:void(0);" class="nav-link"> <a href="{% url "backendni" %}" class="nav-link">
<i class="nav-link-icon fa fa-edit"></i> <i class="nav-link-icon fa fa-comment-dots"></i>
Projects Chat
</a> </a>
</li> </li>
<li class="dropdown nav-item"> <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> <i class="nav-link-icon fa fa-cog"></i>
Settings User Settings
</a> </a>
</li> </li>
</ul> </div> </ul> </div>
@ -99,10 +99,7 @@
<i class="fa fa-angle-down ml-2 opacity-8"></i> <i class="fa fa-angle-down ml-2 opacity-8"></i>
</a> </a>
<div tabindex="-1" role="menu" aria-hidden="true" class="dropdown-menu dropdown-menu-right"> <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> <a href="{% url "admins_edit" request.user.id %}" type="button" tabindex="0" class="dropdown-item">User Account</a>
<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>
<div tabindex="-1" class="dropdown-divider"></div> <div tabindex="-1" class="dropdown-divider"></div>
<a href="{% url "logout" %}" type="button" tabindex="0" class="dropdown-item">Logout</a> <a href="{% url "logout" %}" type="button" tabindex="0" class="dropdown-item">Logout</a>
</div> </div>
@ -110,7 +107,7 @@
</div> </div>
<div class="widget-content-left ml-3 header-user-info"> <div class="widget-content-left ml-3 header-user-info">
<div class="widget-heading"> <div class="widget-heading">
{{ request.user.get_full_name }} {{ request.user.profile.get_internal_name }}
</div> </div>
<div class="widget-subheading"> <div class="widget-subheading">
{{ request.user.profile.role }} {{ request.user.profile.role }}

View file

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

View file

@ -27,7 +27,7 @@
<div class="card-header-tab card-header-tab-animation card-header"> <div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title"> <div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i> <i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting {{ object.user.username }} Deleting {{ object.get_full_name }}
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -35,7 +35,7 @@
<div class="tab-pane fade show active" id="tabs-eg-77"> <div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST"> <form method="POST">
{% csrf_token %} {% 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 %} {% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success"> <button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save <i class="fa fa-check"></i> Save

View file

@ -52,7 +52,7 @@
<td>{{ admin.role }}</td> <td>{{ admin.role }}</td>
<td>{{ admin.user.username }}</td> <td>{{ admin.user.username }}</td>
<td>{{ admin.mobile }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

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