diff --git a/core/classes/mail.py b/core/classes/mail.py index cd84977..795020e 100644 --- a/core/classes/mail.py +++ b/core/classes/mail.py @@ -30,6 +30,8 @@ class BaseMailProvider: for header, value in headers.items(): if value: message.add_header(header, value) + + message.set_charset("base64") return self.send_message(message) class SMTPMailProvider(BaseMailProvider): @@ -43,4 +45,4 @@ class SMTPMailProvider(BaseMailProvider): return "SMTP Mail" def send_message(self, message): - return self.smtp.send_message(message) + return self.smtp.send_message(message, rcpt_options=['NOTIFY=SUCCESS,DELAY,FAILURE']) diff --git a/mail_templates/pwreset_mail.html b/core/exceptions/__init__.py similarity index 100% rename from mail_templates/pwreset_mail.html rename to core/exceptions/__init__.py diff --git a/core/exceptions/mail.py b/core/exceptions/mail.py new file mode 100644 index 0000000..fd7a658 --- /dev/null +++ b/core/exceptions/mail.py @@ -0,0 +1,2 @@ +class NoSuchTemplate(ValueError): + pass \ No newline at end of file diff --git a/core/helpers/auth.py b/core/helpers/auth.py index d0a3647..97d96ad 100644 --- a/core/helpers/auth.py +++ b/core/helpers/auth.py @@ -1,2 +1,11 @@ +from core.helpers.mail import get_template +from core.helpers.urls import relative_to_absolute as reltoabs + +from django.urls import reverse + +from dbsettings.functions import getValue + def generate_pwreset_mail(user, token): - pass \ No newline at end of file + link = reltoabs(reverse("pwreset", kwargs={"pk": str(token.token)})) + template = get_template("backend/auth/pwreset", first_name=user.first_name, link=link, sitename=getValue("core.title", "Expephalon")) + return template \ No newline at end of file diff --git a/core/helpers/mail.py b/core/helpers/mail.py index 50f4ec7..fc6f98e 100644 --- a/core/helpers/mail.py +++ b/core/helpers/mail.py @@ -1,8 +1,13 @@ -from core.modules.mail import providers +from django.conf import settings + +from core.modules.mail import providers, templates from core.tasks.mail import send_mail as send_mail_task +from core.exceptions.mail import NoSuchTemplate from dbsettings.functions import getValue +import os.path + def get_provider_by_name(name, fallback=True): return providers.get(name, None) or providers["smtp"] @@ -16,5 +21,16 @@ def send_mail(provider=get_default_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 \ No newline at end of file +def get_template(template_name, format="txt", **kwargs): + try: + template = templates[template_name][format] + except KeyError: + raise NoSuchTemplate(f"No email template called {template_name} of format {format} loaded") + + with open(template, "r") as templatefile: + templatetext = templatefile.read() + + for key, value in kwargs.items(): + templatetext = templatetext.replace('{§%s§}' % key, value) + + return templatetext \ No newline at end of file diff --git a/core/helpers/urls.py b/core/helpers/urls.py new file mode 100644 index 0000000..cd92d59 --- /dev/null +++ b/core/helpers/urls.py @@ -0,0 +1,6 @@ +from urllib.parse import urljoin + +from dbsettings.functions import getValue + +def relative_to_absolute(path, domain=getValue("core.base_url", "http://localhost:8000")): + return urljoin(domain, path) \ No newline at end of file diff --git a/core/mixins/auth.py b/core/mixins/auth.py new file mode 100644 index 0000000..03977d9 --- /dev/null +++ b/core/mixins/auth.py @@ -0,0 +1,18 @@ +from django.contrib.auth.mixins import AccessMixin +from django.contrib.messages import error + +from core.models.profiles import AdminProfile + +class AdminMixin(AccessMixin): + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + self.permission_denied_message = "You must be logged in to access this area." + + else: + try: + AdminProfile.objects.get(user=request.user) + return super().dispatch(request, *args, **kwargs) + except AdminProfile.DoesNotExist: + self.permission_denied_message = "You must be an administrator to access this area." + + return self.handle_no_permission() \ No newline at end of file diff --git a/core/modules/mail.py b/core/modules/mail.py index fcfaa88..d6f1803 100644 --- a/core/modules/mail.py +++ b/core/modules/mail.py @@ -1,10 +1,28 @@ from core.classes.mail import SMTPMailProvider import importlib +import pathlib +import os.path +import logging from django.conf import settings providers = { "smtp": SMTPMailProvider } +templates = {} + +logger = logging.getLogger(__name__) + +for module in settings.EXPEPHALON_MODULES + [""]: + for template in pathlib.Path(os.path.join(settings.BASE_DIR, module, "templates/mail/")).rglob("*.*"): + if os.path.isfile(template): + template_name = str(template).rsplit("templates/mail/")[-1].rsplit(".")[0] + template_format = str(template).rsplit(".")[-1].lower() + if not template_name in templates.keys(): + templates[template_name] = dict() + if template_format in templates[template_name].keys(): + logger.warning("Mail Template %s, that was seen at %s, was also found at %s. Using latter.", + template_name, templates[template_name][template_format], str(template)) + templates[template_name][template_format] = str(template) for module in settings.EXPEPHALON_MODULES: try: diff --git a/core/modules/urls.py b/core/modules/urls.py index 059b4fe..28ba9bc 100644 --- a/core/modules/urls.py +++ b/core/modules/urls.py @@ -10,6 +10,7 @@ from core.views import ( LogoutView, OTPValidatorView, PWResetView, + PWRequestView, BackendNotImplementedView, AdminListView, AdminDeleteView, @@ -29,7 +30,8 @@ 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")) +URLPATTERNS.append(path('login/reset/', PWRequestView.as_view(), name="pwrequest")) +URLPATTERNS.append(path('login/reset//', PWResetView.as_view(), name="pwreset")) # Base Backend URLs diff --git a/core/views/__init__.py b/core/views/__init__.py index e07a4f4..2988aaa 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -5,6 +5,8 @@ from django.conf import settings from core.views.dbsettings import * from core.views.auth import * from core.views.profiles import * +from core.views.generic import * +from core.mixins.auth import AdminMixin # Create your views here. @@ -16,7 +18,7 @@ class IndexView(TemplateView): context["title"] = "Home" return context -class DashboardView(TemplateView): +class DashboardView(BackendTemplateView): template_name = f"{settings.EXPEPHALON_BACKEND}/index.html" def get_context_data(self, **kwargs): @@ -24,10 +26,10 @@ class DashboardView(TemplateView): context["title"] = "Dashboard" return context -class BackendNotImplementedView(TemplateView): +class BackendNotImplementedView(BackendTemplateView): template_name = f"{settings.EXPEPHALON_BACKEND}/notimplemented.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = "Oops!" - return context \ No newline at end of file + return context diff --git a/core/views/auth.py b/core/views/auth.py index 2ea40db..e7235fd 100644 --- a/core/views/auth.py +++ b/core/views/auth.py @@ -9,7 +9,7 @@ from django.utils import timezone from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm 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.mail import send_mail +from core.helpers.mail import simple_send_mail from core.helpers.auth import generate_pwreset_mail from dbsettings.functions import getValue @@ -164,7 +164,10 @@ class PWRequestView(FormView): 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") \ No newline at end of file + mail = generate_pwreset_mail(user, token) + simple_send_mail("Password Reset", mail, user.email) + except: + raise +# 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") \ No newline at end of file diff --git a/core/views/generic.py b/core/views/generic.py new file mode 100644 index 0000000..64c3379 --- /dev/null +++ b/core/views/generic.py @@ -0,0 +1,17 @@ +from django.views.generic import TemplateView, ListView, CreateView, FormView, DeleteView +from core.mixins.auth import AdminMixin + +class BackendTemplateView(AdminMixin, TemplateView): + pass + +class BackendListView(AdminMixin, ListView): + pass + +class BackendCreateView(AdminMixin, CreateView): + pass + +class BackendFormView(AdminMixin, FormView): + pass + +class BackendDeleteView(AdminMixin, DeleteView): + pass \ No newline at end of file diff --git a/core/views/profiles.py b/core/views/profiles.py index ef7616b..dfd2f0f 100644 --- a/core/views/profiles.py +++ b/core/views/profiles.py @@ -1,10 +1,10 @@ from django.conf import settings -from django.views.generic import FormView, ListView, DeleteView from django.urls import reverse_lazy from django.contrib.auth import get_user_model from core.models import AdminProfile from core.forms import AdminEditForm +from core.views.generic import BackendFormView as FormView, BackendListView as ListView, BackendDeleteView as DeleteView class AdminListView(ListView): template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html" diff --git a/expephalon/celery.py b/expephalon/celery.py index 489d160..2ae3c51 100644 --- a/expephalon/celery.py +++ b/expephalon/celery.py @@ -16,7 +16,6 @@ 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)) \ No newline at end of file diff --git a/expephalon/settings.py b/expephalon/settings.py index bd9ab28..6cdd7b6 100644 --- a/expephalon/settings.py +++ b/expephalon/settings.py @@ -1,5 +1,7 @@ import os +from django.urls import reverse_lazy + from expephalon.custom_settings import * # pylint: disable=unused-wildcard-import # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -141,6 +143,14 @@ if MEMCACHED_LOCATION: CELERY_TASK_SERIALIZER = "pickle" CELERY_RESULT_SERIALIZER = "pickle" CELERY_ACCEPT_CONTENT = ['pickle'] -CELERY_CACHE_BACKEND = 'default' +CELERY_RESULT_BACKEND = 'django-db' +CELERY_CACHE_BACKEND = 'django-cache' CELERY_BROKER_URL = f"amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_LOCATION}/{RABBITMQ_VHOST}" -CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" \ No newline at end of file +CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" +CELERY_TASK_RESULT_EXPIRES = 12 * 60 * 60 + +# Auth URLs + +LOGIN_REDIRECT_URL = reverse_lazy('dashboard') +LOGIN_URL = reverse_lazy('login') +LOGOUT_URL = reverse_lazy('logout') \ No newline at end of file diff --git a/mail_templates/pwreset_mail.txt b/mail_templates/pwreset_mail.txt deleted file mode 100644 index 45cb109..0000000 --- a/mail_templates/pwreset_mail.txt +++ /dev/null @@ -1,11 +0,0 @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3320023..140a2c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ git+https://kumig.it/kumisystems/django-dbsettings.git celery django-celery-results django-celery-beat +python-memcached diff --git a/templates/backend/auth/login.html b/templates/backend/auth/login.html index 309a28f..9c56c3f 100644 --- a/templates/backend/auth/login.html +++ b/templates/backend/auth/login.html @@ -24,7 +24,7 @@
-
Recover Password +
Recover Password
diff --git a/templates/mail/backend/auth/pwreset.html b/templates/mail/backend/auth/pwreset.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/mail/backend/auth/pwreset.txt b/templates/mail/backend/auth/pwreset.txt new file mode 100644 index 0000000..76c8073 --- /dev/null +++ b/templates/mail/backend/auth/pwreset.txt @@ -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 \ No newline at end of file