diff --git a/.envs/.example b/.envs/.example index 98e5726d..01605030 100644 --- a/.envs/.example +++ b/.envs/.example @@ -37,6 +37,8 @@ POSTGRES_PASSWORD=secret # Email server variables SMTP_SERVER=mailpit SMTP_PORT=1025 +SMTP_USE_TLS=False +SMTP_USE_SSL=False SMTP_USER= SMTP_PASSWORD= SMTP_SENDER_MAIL="jandig@memelab.com.br" diff --git a/etc/scripts/compilemessages.py b/etc/scripts/compilemessages.py index 0521787e..1cda8124 100644 --- a/etc/scripts/compilemessages.py +++ b/etc/scripts/compilemessages.py @@ -15,7 +15,7 @@ def main(): """ # Walk entire tree, looking for locale directories basedirs = ["locale"] - for dirpath, dirnames, filenames in os.walk(".", topdown=True): + for dirpath, dirnames, filenames in os.walk("/jandig/locale", topdown=True): for dirname in dirnames: if dirname == "locale": basedirs.append(os.path.join(dirpath, dirname)) diff --git a/src/config/settings.py b/src/config/settings.py index dc8c79b7..c8f5dc3e 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _ from sentry_sdk.integrations.django import DjangoIntegration -from .storage_settings import * # noqa F403 F401 +from .storage_settings import * # noqa F403 F401 ROOT_DIR = environ.Path("/jandig/") BASE_DIR = "/jandig/src" @@ -194,11 +194,15 @@ def debug(request): # Sphinx docs DOCS_ROOT = "/jandig/build/" -SMTP_SERVER = env("SMTP_SERVER", default="mailpit") -SMTP_PORT = env("SMTP_PORT", default=1025) -SMTP_USER = env("SMTP_USER", default="jandig@jandig.com") -SMTP_PASSWORD = env("SMTP_PASSWORD", default="password") -SMTP_SENDER_MAIL = env("SMTP_SENDER_MAIL", default="jandig@memelab.com.br") + +DEFAULT_FROM_EMAIL = env("SMTP_SENDER_MAIL", default="jandig@memelab.com.br") +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = env("SMTP_SERVER", default="mailpit") +EMAIL_USE_TLS = env("SMTP_USE_TLS", default=False) +EMAIL_PORT = env("SMTP_PORT", default=1025) +EMAIL_HOST_USER = env("SMTP_USER", default="jandig@jandig.com") +EMAIL_HOST_PASSWORD = env("SMTP_PASSWORD", default="password") +EMAIL_USE_SSL = False # Recaptcha RECAPTCHA_ENABLED = env("RECAPTCHA_ENABLED", default=False) diff --git a/src/core/urls.py b/src/core/urls.py index 72f94734..766a916e 100644 --- a/src/core/urls.py +++ b/src/core/urls.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.urls import include, path +from django.urls import include, path, re_path from rest_framework_nested.routers import DefaultRouter from core.views.artworks import ArtworkViewset @@ -47,7 +47,11 @@ path("manifest.json", manifest, name="manifest"), path("upload", upload_image, name="upload-image"), path("i18n/", include("django.conf.urls.i18n")), - path("see_all/", see_all, name="see_all"), + re_path( + r"^see_all(?:/(?P[a-zA-Z]+))?(?:/(?P\d+))?/$", + see_all, + name="see_all", + ), path("robots.txt", robots_txt), path("favicon.ico", favicon), path(settings.HEALTH_CHECK_URL, health_check), diff --git a/src/core/views/views.py b/src/core/views/views.py index 5a8a745c..22e33593 100644 --- a/src/core/views/views.py +++ b/src/core/views/views.py @@ -51,12 +51,21 @@ def collection(request): @cache_page(60 * 2) @require_http_methods(["GET"]) -def see_all(request): - request_type = request.GET.get("which") +def see_all(request, which="", page=1): + request_type = request.GET.get("which", which) + if request_type not in ["objects", "markers", "artworks", "exhibits"]: + # Invalid request type, return to collection + return redirect("collection") ctx = {} - per_page = 20 + per_page = 3 page = request.GET.get("page", 1) + try: + # Bots insert random strings in the page parameter + page = int(page) + except ValueError: + page = 1 + data_types = { "objects": Object.objects.all().order_by("uploaded_at"), "markers": Marker.objects.all().order_by("uploaded_at"), @@ -67,10 +76,12 @@ def see_all(request): data = data_types.get(request_type) if data: paginator = Paginator(data, per_page) - data = paginator.get_page(page) - data.adjusted_elided_pages = paginator.get_elided_page_range(page) + if page > paginator.num_pages: + return redirect("see_all", request_type, paginator.num_pages) + paginated_data = paginator.get_page(page) + paginated_data.adjusted_elided_pages = paginator.get_elided_page_range(page) ctx = { - request_type: data, + request_type: paginated_data, "seeall": True, } diff --git a/src/users/forms.py b/src/users/forms.py index 172bfee1..7fb3b6ec 100644 --- a/src/users/forms.py +++ b/src/users/forms.py @@ -171,14 +171,6 @@ def clean(self): return cleaned_data -class RecoverPasswordForm(forms.Form): - username_or_email = forms.CharField(label="username / email", max_length="50") - - -class RecoverPasswordCodeForm(forms.Form): - verification_code = forms.CharField(label="Verification code", max_length="200") - - class UploadMarkerForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(UploadMarkerForm, self).__init__(*args, **kwargs) diff --git a/src/users/jinja2/users/invalid-recovering-email.jinja2 b/src/users/jinja2/users/invalid-recovering-email.jinja2 deleted file mode 100644 index c83aeafa..00000000 --- a/src/users/jinja2/users/invalid-recovering-email.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -{% extends '/core/arviewer.jinja2' %} - -{% block content %} - -
- - - -
-{% endblock %} \ No newline at end of file diff --git a/src/users/jinja2/users/login.jinja2 b/src/users/jinja2/users/login.jinja2 index 2b720ba0..451a8114 100644 --- a/src/users/jinja2/users/login.jinja2 +++ b/src/users/jinja2/users/login.jinja2 @@ -46,7 +46,7 @@ diff --git a/src/users/jinja2/users/recover-edit-password.jinja2 b/src/users/jinja2/users/recover-edit-password.jinja2 deleted file mode 100644 index 1d0068a9..00000000 --- a/src/users/jinja2/users/recover-edit-password.jinja2 +++ /dev/null @@ -1,31 +0,0 @@ -{% extends '/core/arviewer.jinja2' %} - -{% block content %} - -
- - - -
-{% endblock %} \ No newline at end of file diff --git a/src/users/jinja2/users/recover-password-code.jinja2 b/src/users/jinja2/users/recover-password-code.jinja2 deleted file mode 100644 index 79fa95ad..00000000 --- a/src/users/jinja2/users/recover-password-code.jinja2 +++ /dev/null @@ -1,31 +0,0 @@ -{% extends '/core/arviewer.jinja2' %} - -{% block content %} - -
- - - -
-{% endblock %} \ No newline at end of file diff --git a/src/users/jinja2/users/recover-password.jinja2 b/src/users/jinja2/users/recover-password.jinja2 deleted file mode 100644 index 1d8efac2..00000000 --- a/src/users/jinja2/users/recover-password.jinja2 +++ /dev/null @@ -1,46 +0,0 @@ -{% extends '/core/arviewer.jinja2' %} - -{% block extra_css%} - -{% endblock %} - -{% block extra_js%} - - {% if recaptcha_enabled %} - - - {% endif %} -{% endblock %} - -{% block content %} -
- - -
-{% endblock %} \ No newline at end of file diff --git a/src/users/jinja2/users/reset-password/password_reset.jinja2 b/src/users/jinja2/users/reset-password/password_reset.jinja2 new file mode 100644 index 00000000..18146e6a --- /dev/null +++ b/src/users/jinja2/users/reset-password/password_reset.jinja2 @@ -0,0 +1,59 @@ +{% extends '/core/arviewer.jinja2' %} +{% block content %} +
+
+
+
+
+
+
+

Forgot Password?

+
+ {% if form.errors %} + + {% endif %} +
+
+ {{ csrf_input }} +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/src/users/jinja2/users/reset-password/password_reset_complete.jinja2 b/src/users/jinja2/users/reset-password/password_reset_complete.jinja2 new file mode 100644 index 00000000..d878ed47 --- /dev/null +++ b/src/users/jinja2/users/reset-password/password_reset_complete.jinja2 @@ -0,0 +1,15 @@ +{% extends '/core/arviewer.jinja2' %} +{% block title %} Password Reset {% endblock title%} +{% block content %} +
+
+
+
+
+ Your password has been set. You may go ahead and Login Here +
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/src/users/jinja2/users/reset-password/password_reset_confirm.jinja2 b/src/users/jinja2/users/reset-password/password_reset_confirm.jinja2 new file mode 100644 index 00000000..ccdef32d --- /dev/null +++ b/src/users/jinja2/users/reset-password/password_reset_confirm.jinja2 @@ -0,0 +1,71 @@ +{% extends '/core/arviewer.jinja2' %} +{% block title %} Password Reset {% endblock title%} +{% block content %} +
+
+
+
+ {% if validlink %} +
+
+

Reset Your Password

+
+ {% if form.errors %} + + {% endif %} + +
+
+ {{ csrf_input }} +
+
+
+ + + + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+ {% else %} +
+ The password reset link was invalid, possibly because it has already been used. + Please request a new password reset. +
+ {% endif %} +
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/src/users/jinja2/users/reset-password/password_reset_email.html b/src/users/jinja2/users/reset-password/password_reset_email.html new file mode 100644 index 00000000..e00e10f0 --- /dev/null +++ b/src/users/jinja2/users/reset-password/password_reset_email.html @@ -0,0 +1,11 @@ + + To initiate the password reset process for your {{ user.email }} Jandig account, + click the link below: + + {{ protocol }}://{{ domain }}{{ url('password_reset_confirm',args=[uid,token]) }} + + If clicking the link above doesn't work, please copy and paste the URL in a new browser + window instead. + + Sincerely, + Jandig Team diff --git a/src/users/jinja2/users/reset-password/password_reset_subject.txt b/src/users/jinja2/users/reset-password/password_reset_subject.txt new file mode 100644 index 00000000..5cb9df8e --- /dev/null +++ b/src/users/jinja2/users/reset-password/password_reset_subject.txt @@ -0,0 +1 @@ +Jandig Password Reset \ No newline at end of file diff --git a/src/users/jinja2/users/wrong-verification-code.jinja2 b/src/users/jinja2/users/wrong-verification-code.jinja2 deleted file mode 100644 index b3a8ddaf..00000000 --- a/src/users/jinja2/users/wrong-verification-code.jinja2 +++ /dev/null @@ -1,22 +0,0 @@ -{% extends '/core/arviewer.jinja2' %} - -{% block content %} - -
- - - -
-{% endblock %} \ No newline at end of file diff --git a/src/users/services/__init__.py b/src/users/services/__init__.py index e69de29b..744f9d1c 100644 --- a/src/users/services/__init__.py +++ b/src/users/services/__init__.py @@ -0,0 +1 @@ +from .recaptcha_service import * # noqa diff --git a/src/users/services/email_service.py b/src/users/services/email_service.py deleted file mode 100644 index fd23ec18..00000000 --- a/src/users/services/email_service.py +++ /dev/null @@ -1,35 +0,0 @@ -import smtplib -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -from django.conf import settings - - -class EmailService: - def __init__(self, email_message): - self.smtp_server = settings.SMTP_SERVER - self.smtp_port = settings.SMTP_PORT - self.smtp_user = settings.SMTP_USER - self.smtp_password = settings.SMTP_PASSWORD - self.jandig_email = settings.SMTP_SENDER_MAIL - self.email_message = email_message - - def send_email_to_recover_password(self, multipart_message): - email_server = smtplib.SMTP(self.smtp_server, self.smtp_port) - email_server.starttls() - email_server.login(self.smtp_user, self.smtp_password) - email_server.sendmail( - multipart_message["From"], - multipart_message["To"], - multipart_message.as_string(), - ) - email_server.quit() - - def build_multipart_message(self, user_email): - multipart_message = MIMEMultipart("alternative") - multipart_message["From"] = f"Jandig <{self.jandig_email}>" - multipart_message["To"] = "{}".format(user_email) - multipart_message["Subject"] = "Recover Password" - - multipart_message.attach(MIMEText(self.email_message, "plain")) - return multipart_message diff --git a/src/users/services/encrypt_service.py b/src/users/services/encrypt_service.py deleted file mode 100644 index 6a8e0d1d..00000000 --- a/src/users/services/encrypt_service.py +++ /dev/null @@ -1,24 +0,0 @@ -import hashlib -import secrets -from datetime import datetime - - -class EncryptService: - def generate_verification_code(self, email): - datetime_now = datetime.now() - _year = datetime_now.year - _month = datetime_now.month - _day = datetime_now.day - _hour = datetime_now.hour - _minute = datetime_now.minute - _second = datetime_now.second - _microsec = datetime_now.microsecond - - today = f"{_year}{_month}{_day}{_hour}{_minute}{_second}{_microsec}" - decrypt_code = str(today) + (email * 4) + secrets.token_hex(16) - verification_code = self.generate_hash_code(decrypt_code) - return verification_code - - def generate_hash_code(self, decrypt_code): - hash_code = hashlib.sha256(bytes(decrypt_code, encoding="utf-8")) - return hash_code.hexdigest() diff --git a/src/users/services/user_service.py b/src/users/services/user_service.py deleted file mode 100644 index b79df83e..00000000 --- a/src/users/services/user_service.py +++ /dev/null @@ -1,23 +0,0 @@ -import logging - -from django.contrib.auth.models import User - -log = logging.getLogger("ej") - - -class UserService: - def get_user_email(self, username_or_email): - if "@" in username_or_email: - return username_or_email - user = User.objects.get(username=username_or_email) - log.warning(user) - return user.email - - def check_if_username_or_email_exist(self, username_or_email): - if "@" in username_or_email: - if not User.objects.filter(email=username_or_email).exists(): - return False - else: - if not User.objects.filter(username=username_or_email).exists(): - return False - return True diff --git a/src/users/urls.py b/src/users/urls.py index a4fcb038..4315f6bc 100644 --- a/src/users/urls.py +++ b/src/users/urls.py @@ -3,6 +3,7 @@ from .forms import LoginForm from .views import ( + ResetPasswordView, create_artwork, create_exhibit, delete, @@ -14,19 +15,14 @@ edit_password, edit_profile, element_get, - invalid_recovering_email_or_username, marker_upload, mod, mod_delete, object_upload, permission_denied, profile, - recover_code, - recover_edit_password, - recover_password, related_content, signup, - wrong_verification_code, ) urlpatterns = [ @@ -40,22 +36,24 @@ name="login", ), path("logout/", auth_views.LogoutView.as_view(), name="logout"), - path("recover/", recover_password, name="recover"), - path("recover-code/", recover_code, name="recover-code"), - path("profile/", profile, name="profile"), - path("profile/edit/", edit_profile, name="edit-profile"), - path("profile/edit-password/", edit_password, name="edit-password"), + path("reset-password/", ResetPasswordView.as_view(), name="reset-password"), path( - "wrong-verification-code", - wrong_verification_code, - name="wrong-verification-code", + "password-reset-confirm///", + auth_views.PasswordResetConfirmView.as_view( + template_name="users/reset-password/password_reset_confirm.jinja2" + ), + name="password_reset_confirm", ), path( - "invalid-recovering-email", - invalid_recovering_email_or_username, - name="invalid_recovering_email_or_username", + "password-reset-complete/", + auth_views.PasswordResetCompleteView.as_view( + template_name="users/reset-password/password_reset_complete.jinja2" + ), + name="password_reset_complete", ), - path("recover-edit-password", recover_edit_password, name="recover-edit-password"), + path("profile/", profile, name="profile"), + path("profile/edit/", edit_profile, name="edit-profile"), + path("profile/edit-password/", edit_password, name="edit-password"), path("markers/upload/", marker_upload, name="marker-upload"), path("objects/upload/", object_upload, name="object-upload"), path("element/get/", element_get, name="element-get"), diff --git a/src/users/views.py b/src/users/views.py index fb60ecf7..3b7609fb 100644 --- a/src/users/views.py +++ b/src/users/views.py @@ -9,9 +9,12 @@ update_session_auth_hash, ) from django.contrib.auth.decorators import login_required -from django.contrib.auth.forms import SetPasswordForm +from django.contrib.auth.views import PasswordResetView +from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import cache_page from django.views.decorators.http import require_http_methods @@ -22,20 +25,17 @@ ExhibitForm, PasswordChangeForm, ProfileForm, - RecoverPasswordCodeForm, - RecoverPasswordForm, SignupForm, UploadMarkerForm, UploadObjectForm, ) from .models import Profile -from .services.email_service import EmailService -from .services.encrypt_service import EncryptService -from .services.recaptcha_service import BOT_SCORE, create_assessment -from .services.user_service import UserService +from .services import BOT_SCORE, create_assessment log = logging.getLogger(__file__) +User = get_user_model() + def signup(request): if request.method == "POST": @@ -72,109 +72,17 @@ def signup(request): ) -User = get_user_model() - - -def recover_password(request): - if request.method == "POST": - if settings.RECAPTCHA_ENABLED: - recaptcha_token = request.POST.get("g-recaptcha-response") - assessment = create_assessment( - token=recaptcha_token, recaptcha_action="recover_password" - ) - score = assessment.get("riskAnalysis", {}).get("score", -1) - if score <= BOT_SCORE: - return redirect("home") - - recover_password_form = RecoverPasswordForm(request.POST) - - if recover_password_form.is_valid(): - username_or_email = recover_password_form.cleaned_data.get( - "username_or_email" - ) - user_service = UserService() - username_or_email_is_valid = user_service.check_if_username_or_email_exist( - username_or_email - ) - if not username_or_email_is_valid: - return redirect("invalid_recovering_email_or_username") - - global global_recovering_email - global_recovering_email = user_service.get_user_email(username_or_email) - - global global_verification_code - encrypt_service = EncryptService() - global_verification_code = encrypt_service.generate_verification_code( - global_recovering_email - ) - - build_message_and_send_to_user(global_recovering_email) - - return redirect("recover-code") - - recover_password_form = RecoverPasswordForm() - return render( - request, - "users/recover-password.jinja2", - { - "form": recover_password_form, - "recaptcha_enabled": settings.RECAPTCHA_ENABLED, - "recaptcha_site_key": settings.RECAPTCHA_SITE_KEY, - }, +class ResetPasswordView(SuccessMessageMixin, PasswordResetView): + template_name = "users/reset-password/password_reset.jinja2" + email_template_name = "users/reset-password/password_reset_email.html" + subject_template_name = "users/reset-password/password_reset_subject.txt" + success_message = _( + "We've emailed you instructions for setting your password, " + "if an account exists with the email you entered. You should receive them shortly." + " If you don't receive an email, " + "please make sure you've entered the address you registered with, and check your spam folder." ) - - -def build_message_and_send_to_user(email): - message = f"You have requested a new password. This is your verification code: {global_verification_code}\nCopy it and put into the field." - email_service = EmailService(message) - multipart_message = email_service.build_multipart_message(email) - email_service.send_email_to_recover_password(multipart_message) - - -def recover_code(request): - if request.method == "POST": - form = RecoverPasswordCodeForm(request.POST) - - if form.is_valid(): - code = form.cleaned_data.get("verification_code") - - log.warning("Inserido: %s", code) - log.warning("Correto: %s", global_verification_code) - - if code == global_verification_code: - global recover_password_user - recover_password_user = User.objects.get(email=global_recovering_email) - return redirect("recover-edit-password") - - return redirect("wrong-verification-code") - return redirect("home") - - form = RecoverPasswordCodeForm() - return render(request, "users/recover-password-code.jinja2", {"form": form}) - - -def recover_edit_password(request): - if request.method == "POST": - form = SetPasswordForm(recover_password_user, data=request.POST) - - if form.is_valid(): - form.save() - - return redirect("login") - else: - form = SetPasswordForm(recover_password_user) - - return render(request, "users/recover-edit-password.jinja2", {"form": form}) - - -@require_http_methods(["GET"]) -def wrong_verification_code(request): - return render(request, "users/wrong-verification-code.jinja2") - - -@require_http_methods(["GET"]) -def invalid_recovering_email_or_username(request): - return render(request, "users/invalid-recovering-email.jinja2") + success_url = reverse_lazy("home") @login_required