From b5c816e7488c057ecb4dd70f2e178e95f837107f Mon Sep 17 00:00:00 2001 From: Kumi Date: Sat, 16 Nov 2024 15:22:46 +0100 Subject: [PATCH] feat: Add initial Synapse registration Django app Sets up a Django application for user registration with Synapse, including database models, forms, views, and templates. Introduces functionality for user registration approval and email verification. Configures Django project settings, URLs, and email handling. Includes a sample configuration file and .gitignore additions. --- .gitignore | 8 + .vscode/launch.json | 17 ++ LICENSE | 19 ++ README.md | 0 config.dist.yaml | 14 ++ pyproject.toml | 29 +++ src/synapse_registration/manage.py | 22 +++ .../registration/__init__.py | 0 .../registration/admin.py | 22 +++ src/synapse_registration/registration/apps.py | 9 + .../registration/forms.py | 46 +++++ .../registration/migrations/0001_initial.py | 40 ++++ ...2_alter_userregistration_email_and_more.py | 23 +++ .../registration/migrations/__init__.py | 0 .../registration/models.py | 25 +++ .../registration/signals.py | 29 +++ .../registration/templates/base.html | 24 +++ .../registration/templates/error_page.html | 16 ++ .../registration/templates/landing_page.html | 30 +++ .../registration/already_verified.html | 7 + .../registration/complete_registration.html | 26 +++ .../templates/registration/email_form.html | 16 ++ .../templates/registration/email_sent.html | 7 + .../registration/registration_forbidden.html | 9 + .../registration/registration_pending.html | 9 + .../templates/registration/username_form.html | 16 ++ .../registration/tests.py | 3 + src/synapse_registration/registration/urls.py | 26 +++ .../registration/views.py | 132 +++++++++++++ .../synapse_registration/__init__.py | 0 .../synapse_registration/asgi.py | 16 ++ .../synapse_registration/settings.py | 183 ++++++++++++++++++ .../synapse_registration/urls.py | 23 +++ .../synapse_registration/wsgi.py | 16 ++ 34 files changed, 862 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.dist.yaml create mode 100644 pyproject.toml create mode 100755 src/synapse_registration/manage.py create mode 100644 src/synapse_registration/registration/__init__.py create mode 100644 src/synapse_registration/registration/admin.py create mode 100644 src/synapse_registration/registration/apps.py create mode 100644 src/synapse_registration/registration/forms.py create mode 100644 src/synapse_registration/registration/migrations/0001_initial.py create mode 100644 src/synapse_registration/registration/migrations/0002_alter_userregistration_email_and_more.py create mode 100644 src/synapse_registration/registration/migrations/__init__.py create mode 100644 src/synapse_registration/registration/models.py create mode 100644 src/synapse_registration/registration/signals.py create mode 100644 src/synapse_registration/registration/templates/base.html create mode 100644 src/synapse_registration/registration/templates/error_page.html create mode 100644 src/synapse_registration/registration/templates/landing_page.html create mode 100644 src/synapse_registration/registration/templates/registration/already_verified.html create mode 100644 src/synapse_registration/registration/templates/registration/complete_registration.html create mode 100644 src/synapse_registration/registration/templates/registration/email_form.html create mode 100644 src/synapse_registration/registration/templates/registration/email_sent.html create mode 100644 src/synapse_registration/registration/templates/registration/registration_forbidden.html create mode 100644 src/synapse_registration/registration/templates/registration/registration_pending.html create mode 100644 src/synapse_registration/registration/templates/registration/username_form.html create mode 100644 src/synapse_registration/registration/tests.py create mode 100644 src/synapse_registration/registration/urls.py create mode 100644 src/synapse_registration/registration/views.py create mode 100644 src/synapse_registration/synapse_registration/__init__.py create mode 100644 src/synapse_registration/synapse_registration/asgi.py create mode 100644 src/synapse_registration/synapse_registration/settings.py create mode 100644 src/synapse_registration/synapse_registration/urls.py create mode 100644 src/synapse_registration/synapse_registration/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9be499 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +venv/ +.venv/ +__pycache__/ +*.pyc +/dist/ +config.yaml +db.sqlite3 +db.sqlite3-journal diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b7ace68 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Django", + "type": "debugpy", + "request": "launch", + "args": ["runserver", "8117"], + "django": true, + "autoStartBrowser": false, + "program": "${workspaceFolder}/venv/bin/synapse_registration" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3788438 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Private.coffee Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config.dist.yaml b/config.dist.yaml new file mode 100644 index 0000000..943208c --- /dev/null +++ b/config.dist.yaml @@ -0,0 +1,14 @@ +synapse: + admin_token: syt_your_admin_token + server: https://matrix.your.server + domain: your.server # i.e. the part after the : in your matrix ID +hosts: + - register.matrix.your.server +email: + host: mail.your.server + port: 587 + username: registrations@your.server + password: your_password + tls: true +admin: + email: admin@your.server \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3abf5de --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "synapse_registration" +version = "0.1.0" +authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }] +description = "A Django app for allowing users to register for a Synapse account." +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.12" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = ["Django", "pyyaml", "requests"] + +[project.scripts] +synapse_registration = "synapse_registration.manage:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/synapse_registration"] + +[project.urls] +"Homepage" = "https://git.private.coffee/privatecoffee/synapse-registration" +"Bug Tracker" = "https://git.private.coffee/privatecoffee/synapse-registration/issues" +"Source Code" = "https://git.private.coffee/privatecoffee/synapse-registration" diff --git a/src/synapse_registration/manage.py b/src/synapse_registration/manage.py new file mode 100755 index 0000000..d2b7b29 --- /dev/null +++ b/src/synapse_registration/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'synapse_registration.synapse_registration.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/src/synapse_registration/registration/__init__.py b/src/synapse_registration/registration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/synapse_registration/registration/admin.py b/src/synapse_registration/registration/admin.py new file mode 100644 index 0000000..cb17ff5 --- /dev/null +++ b/src/synapse_registration/registration/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin + +from .models import UserRegistration + + +@admin.register(UserRegistration) +class UserRegistrationAdmin(admin.ModelAdmin): + list_display = ("username", "email", "email_verified", "status", "ip_address") + list_filter = ("status", "email_verified") + search_fields = ("username", "email", "ip_address") + actions = ["approve_registrations", "deny_registrations"] + + def approve_registrations(self, request, queryset): + queryset.update(status=UserRegistration.STATUS_APPROVED) + self.message_user(request, f"{queryset.count()} registrations approved.") + + def deny_registrations(self, request, queryset): + queryset.update(status=UserRegistration.STATUS_DENIED) + self.message_user(request, f"{queryset.count()} registrations denied.") + + approve_registrations.short_description = "Approve selected registrations" + deny_registrations.short_description = "Deny selected registrations" diff --git a/src/synapse_registration/registration/apps.py b/src/synapse_registration/registration/apps.py new file mode 100644 index 0000000..0726897 --- /dev/null +++ b/src/synapse_registration/registration/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class RegistrationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "synapse_registration.registration" + + def ready(self): + import synapse_registration.registration.signals # noqa: F401 diff --git a/src/synapse_registration/registration/forms.py b/src/synapse_registration/registration/forms.py new file mode 100644 index 0000000..9213d28 --- /dev/null +++ b/src/synapse_registration/registration/forms.py @@ -0,0 +1,46 @@ +from django import forms + + +class UsernameForm(forms.Form): + username = forms.CharField( + max_length=150, + widget=forms.TextInput( + attrs={"class": "input", "placeholder": "Enter your desired username"} + ), + ) + + +class EmailForm(forms.Form): + email = forms.EmailField( + widget=forms.EmailInput( + attrs={"class": "input", "placeholder": "Enter your email address"} + ) + ) + + +class RegistrationForm(forms.Form): + password1 = forms.CharField( + widget=forms.PasswordInput( + attrs={"class": "input", "placeholder": "Enter password"} + ) + ) + password2 = forms.CharField( + widget=forms.PasswordInput( + attrs={"class": "input", "placeholder": "Re-enter password"} + ) + ) + registration_reason = forms.CharField( + widget=forms.Textarea( + attrs={ + "class": "textarea", + "placeholder": "Why do you want to join our server? If you were referred by a current member, who referred you? If you found us through a different means, how did you find us?", + } + ) + ) + + def clean(self): + cleaned_data = super().clean() + password1 = cleaned_data.get("password1") + password2 = cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + self.add_error("password2", "Passwords do not match.") diff --git a/src/synapse_registration/registration/migrations/0001_initial.py b/src/synapse_registration/registration/migrations/0001_initial.py new file mode 100644 index 0000000..55a883c --- /dev/null +++ b/src/synapse_registration/registration/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.3 on 2024-11-16 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="UserRegistration", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("username", models.CharField(max_length=150, unique=True)), + ("email", models.EmailField(max_length=254, unique=True)), + ("registration_reason", models.TextField()), + ("ip_address", models.GenericIPAddressField()), + ( + "status", + models.IntegerField( + choices=[(1, "Requested"), (2, "Approved"), (3, "Denied")], + default=1, + ), + ), + ("token", models.CharField(max_length=64, unique=True)), + ("email_verified", models.BooleanField(default=False)), + ], + ), + ] diff --git a/src/synapse_registration/registration/migrations/0002_alter_userregistration_email_and_more.py b/src/synapse_registration/registration/migrations/0002_alter_userregistration_email_and_more.py new file mode 100644 index 0000000..c2ff4d2 --- /dev/null +++ b/src/synapse_registration/registration/migrations/0002_alter_userregistration_email_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.3 on 2024-11-16 13:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registration", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="userregistration", + name="email", + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name="userregistration", + name="username", + field=models.CharField(max_length=150), + ), + ] diff --git a/src/synapse_registration/registration/migrations/__init__.py b/src/synapse_registration/registration/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/synapse_registration/registration/models.py b/src/synapse_registration/registration/models.py new file mode 100644 index 0000000..5dd1e84 --- /dev/null +++ b/src/synapse_registration/registration/models.py @@ -0,0 +1,25 @@ +from django.db import models + +class UserRegistration(models.Model): + # Status constants + STATUS_REQUESTED = 1 + STATUS_APPROVED = 2 + STATUS_DENIED = 3 + + # Status choices + STATUS_CHOICES = [ + (STATUS_REQUESTED, 'Requested'), + (STATUS_APPROVED, 'Approved'), + (STATUS_DENIED, 'Denied'), + ] + + username = models.CharField(max_length=150) + email = models.EmailField() + registration_reason = models.TextField() + ip_address = models.GenericIPAddressField() + status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_REQUESTED) + token = models.CharField(max_length=64, unique=True) + email_verified = models.BooleanField(default=False) + + def __str__(self): + return self.username diff --git a/src/synapse_registration/registration/signals.py b/src/synapse_registration/registration/signals.py new file mode 100644 index 0000000..8d3333f --- /dev/null +++ b/src/synapse_registration/registration/signals.py @@ -0,0 +1,29 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.core.mail import send_mail +from django.conf import settings +from .models import UserRegistration + + +@receiver(post_save, sender=UserRegistration) +def handle_status_change(sender, instance, created, **kwargs): + if not created: + status = instance.status + + if status == UserRegistration.STATUS_APPROVED: + send_mail( + "Registration Approved", + f"Congratulations, {instance.username}! Your registration has been approved.", + settings.DEFAULT_FROM_EMAIL, + [instance.email], + ) + # TODO: Unlock the user in Synapse + + elif status == UserRegistration.STATUS_DENIED: + send_mail( + "Registration Denied", + f"Sorry, {instance.username}. Your registration request has been denied.", + settings.DEFAULT_FROM_EMAIL, + [instance.email], + ) + # TODO: Deactivate the user in Synapse diff --git a/src/synapse_registration/registration/templates/base.html b/src/synapse_registration/registration/templates/base.html new file mode 100644 index 0000000..8742e6f --- /dev/null +++ b/src/synapse_registration/registration/templates/base.html @@ -0,0 +1,24 @@ + + + + + + + {% block title %} + Registration + {% endblock title %} + + + {% block css %} + {% endblock css %} + + +
+
+ {% block content %} + {% endblock content %} +
+
+ + diff --git a/src/synapse_registration/registration/templates/error_page.html b/src/synapse_registration/registration/templates/error_page.html new file mode 100644 index 0000000..6cd5719 --- /dev/null +++ b/src/synapse_registration/registration/templates/error_page.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %} + Error +{% endblock title %} +{% block content %} +
+ + An error has occurred. Please try again later or contact support. +
+
+

Something Went Wrong

+

+ We're sorry, but an unexpected error has occurred. Please try reloading the page, or return to the homepage. +

+
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/landing_page.html b/src/synapse_registration/registration/templates/landing_page.html new file mode 100644 index 0000000..ee63961 --- /dev/null +++ b/src/synapse_registration/registration/templates/landing_page.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %} + Welcome to Matrix! +{% endblock title %} +{% block content %} +
+
+

Welcome to Our Matrix Server

+

Join us to connect securely and communicate openly across the Matrix network.

+ Start Your Journey +
+
+
+
+

Why Join Our Matrix Server?

+

+ Matrix is an open standard for secure, decentralized, and real-time communication. By joining our server, you gain access to: +

+
    +
  • Secure messaging and VoIP with end-to-end encryption.
  • +
  • Interoperability with other Matrix users and servers globally.
  • +
  • Community and direct chats with powerful, extensible features.
  • +
  • A platform that respects your privacy and data ownership.
  • +
+

+ Whether you're here to chat with friends, collaborate on projects, or explore the potential of decentralized communications, we're excited to have you on board. Click the button above to start your registration and become part of our network. +

+
+
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/already_verified.html b/src/synapse_registration/registration/templates/registration/already_verified.html new file mode 100644 index 0000000..2a6cc45 --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/already_verified.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block title %} + Already Verified +{% endblock title %} +{% block content %} +
Your email is already verified. You can proceed with your registration.
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/complete_registration.html b/src/synapse_registration/registration/templates/registration/complete_registration.html new file mode 100644 index 0000000..5729b57 --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/complete_registration.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %} + Complete Registration +{% endblock title %} +{% block content %} +

Complete Your Registration

+
+ {% csrf_token %} +
+ {{ form.password1.label_tag }} +
{{ form.password1 }}
+

{{ form.password1.errors }}

+
+
+ {{ form.password2.label_tag }} +
{{ form.password2 }}
+

{{ form.password2.errors }}

+
+
+ {{ form.registration_reason.label_tag }} +
{{ form.registration_reason }}
+

{{ form.registration_reason.errors }}

+
+ +
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/email_form.html b/src/synapse_registration/registration/templates/registration/email_form.html new file mode 100644 index 0000000..fcb391e --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/email_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %} + Enter Your Email +{% endblock title %} +{% block content %} +

Enter Your Email

+
+ {% csrf_token %} +
+ {{ form.email.label_tag }} +
{{ form.email }}
+

{{ form.email.errors }}

+
+ +
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/email_sent.html b/src/synapse_registration/registration/templates/registration/email_sent.html new file mode 100644 index 0000000..035bf39 --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/email_sent.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block title %} + Email Sent +{% endblock title %} +{% block content %} +
Thank you! A verification link has been sent to your email.
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/registration_forbidden.html b/src/synapse_registration/registration/templates/registration/registration_forbidden.html new file mode 100644 index 0000000..a2f3614 --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/registration_forbidden.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %} + Access Forbidden +{% endblock title %} +{% block content %} +
+ You cannot complete registration at this time. Please verify your email or check the registration status. +
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/registration_pending.html b/src/synapse_registration/registration/templates/registration/registration_pending.html new file mode 100644 index 0000000..0fcd34c --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/registration_pending.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %} + Registration Submitted +{% endblock title %} +{% block content %} +
+ Your registration is pending approval. We will notify you via email once it is approved. +
+{% endblock content %} diff --git a/src/synapse_registration/registration/templates/registration/username_form.html b/src/synapse_registration/registration/templates/registration/username_form.html new file mode 100644 index 0000000..c89cb48 --- /dev/null +++ b/src/synapse_registration/registration/templates/registration/username_form.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %} + Choose Username +{% endblock title %} +{% block content %} +

Choose a Username

+
+ {% csrf_token %} +
+ {{ form.username.label_tag }} +
{{ form.username }}
+

{{ form.username.errors }}

+
+ +
+{% endblock content %} diff --git a/src/synapse_registration/registration/tests.py b/src/synapse_registration/registration/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/synapse_registration/registration/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/synapse_registration/registration/urls.py b/src/synapse_registration/registration/urls.py new file mode 100644 index 0000000..9d2be39 --- /dev/null +++ b/src/synapse_registration/registration/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from . import views + +urlpatterns = [ + path("", views.LandingPageView.as_view(), name="landing_page"), + path("error/", views.ErrorPageView.as_view(), name="error_page"), + path("check-username/", views.CheckUsernameView.as_view(), name="check_username"), + path("email-input/", views.EmailInputView.as_view(), name="email_input"), + path( + "verify-email//", + views.VerifyEmailView.as_view(), + name="verify_email", + ), + path( + "complete-registration/", + views.CompleteRegistrationView.as_view(), + name="complete_registration", + ), + path( + "registration-complete/", + views.TemplateView.as_view( + template_name="registration/registration_complete.html" + ), + name="registration_complete", + ), +] diff --git a/src/synapse_registration/registration/views.py b/src/synapse_registration/registration/views.py new file mode 100644 index 0000000..82b0c15 --- /dev/null +++ b/src/synapse_registration/registration/views.py @@ -0,0 +1,132 @@ +from django.views.generic import FormView, View, TemplateView +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse_lazy +from django.core.mail import send_mail +from django.conf import settings +from .forms import UsernameForm, EmailForm, RegistrationForm +from .models import UserRegistration +import requests +from secrets import token_urlsafe + + +class LandingPageView(TemplateView): + template_name = "landing_page.html" + + +class ErrorPageView(TemplateView): + template_name = "error_page.html" + + +class CheckUsernameView(FormView): + template_name = "registration/username_form.html" + form_class = UsernameForm + success_url = reverse_lazy("email_input") + + def form_valid(self, form): + username = form.cleaned_data["username"] + response = requests.get( + f"{settings.SYNAPSE_SERVER}/_synapse/admin/v1/username_available?username={username}", + headers={"Authorization": f"Bearer {settings.SYNAPSE_ADMIN_TOKEN}"}, + ) + + if response.json().get("available"): + self.request.session["username"] = username + return super().form_valid(form) + else: + form.add_error("username", "Username is not available.") + return self.form_invalid(form) + + +class EmailInputView(FormView): + template_name = "registration/email_form.html" + form_class = EmailForm + + def form_valid(self, form): + email = form.cleaned_data["email"] + + if UserRegistration.objects.filter(email=email).exists(): + form.add_error( + "email", + "This email is already registered. Please use a different email address.", + ) + return self.form_invalid(form) + + token = token_urlsafe(32) + UserRegistration.objects.create( + username=self.request.session["username"], + email=email, + token=token, + ip_address=self.request.META.get("REMOTE_ADDR"), + ) + verification_link = self.request.build_absolute_uri( + reverse_lazy("verify_email", args=[token]) + ) + send_mail( + "Verify your email", + f"Click the link to verify your email: {verification_link}", + settings.DEFAULT_FROM_EMAIL, + [email], + ) + return render(self.request, "registration/email_sent.html") + + +class VerifyEmailView(View): + def get(self, request, token): + registration = get_object_or_404(UserRegistration, token=token) + request.session["registration"] = registration.id + if registration.email_verified: + return render(request, "registration/already_verified.html") + registration.email_verified = True + registration.save() + return redirect("complete_registration") + + +class CompleteRegistrationView(FormView): + template_name = "registration/complete_registration.html" + form_class = RegistrationForm + success_url = reverse_lazy("registration_complete") + + def form_valid(self, form): + password = form.cleaned_data["password1"] + registration_reason = form.cleaned_data["registration_reason"] + registration = get_object_or_404( + UserRegistration, id=self.request.session.get("registration") + ) + username = registration.username + + response = requests.put( + f"{settings.SYNAPSE_SERVER}/_synapse/admin/v2/users/@{username}:{settings.MATRIX_DOMAIN}", + json={ + "password": password, + "displayname": username, + "threepids": [{"medium": "email", "address": registration.email}], + "locked": True, + }, + headers={"Authorization": f"Bearer {settings.SYNAPSE_ADMIN_TOKEN}"}, + ) + + if response.status_code in (200, 201): + registration.status = UserRegistration.STATUS_REQUESTED + registration.registration_reason = registration_reason + registration.save() + send_mail( + "New Registration Request", + f"Approve the new user {username}", + settings.DEFAULT_FROM_EMAIL, + [settings.ADMIN_EMAIL], + ) + return render(self.request, "registration/registration_pending.html") + + form.add_error(None, "Registration failed.") + return self.form_invalid(form) + + def dispatch(self, request, *args, **kwargs): + self.registration = get_object_or_404( + UserRegistration, id=self.request.session.get("registration") + ) + if ( + self.registration.status != UserRegistration.STATUS_REQUESTED + or not self.registration.email_verified + ): + return render(request, "registration/registration_forbidden.html") + return super().dispatch(request, *args, **kwargs) diff --git a/src/synapse_registration/synapse_registration/__init__.py b/src/synapse_registration/synapse_registration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/synapse_registration/synapse_registration/asgi.py b/src/synapse_registration/synapse_registration/asgi.py new file mode 100644 index 0000000..2c4f8ef --- /dev/null +++ b/src/synapse_registration/synapse_registration/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for synapse_registration project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'synapse_registration.settings') + +application = get_asgi_application() diff --git a/src/synapse_registration/synapse_registration/settings.py b/src/synapse_registration/synapse_registration/settings.py new file mode 100644 index 0000000..1dc2434 --- /dev/null +++ b/src/synapse_registration/synapse_registration/settings.py @@ -0,0 +1,183 @@ +""" +Django settings for synapse_registration project. + +Generated by 'django-admin startproject' using Django 5.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +from django.core.management.utils import get_random_secret_key + +import os + +import yaml + +CONFIG_PATH = os.environ.get("CONFIG_PATH", "config.yaml") + +# Load the configuration file +try: + with open(CONFIG_PATH) as file: + config = yaml.load(file, Loader=yaml.FullLoader) +except FileNotFoundError: + raise FileNotFoundError( + f"Configuration file not found at {CONFIG_PATH} - please copy config.dist.yaml to config.yaml and edit it to suit your needs." + ) + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +if "secret_key" in config: + SECRET_KEY = config["secret_key"] +else: + # Generate a random secret key and write it to the config file + SECRET_KEY = get_random_secret_key() + config["secret_key"] = SECRET_KEY + + with open(CONFIG_PATH, "w") as file: + yaml.dump(config, file) + +DEBUG = config.get("debug", False) + +ALLOWED_HOSTS = config.get("hosts") + +if not ALLOWED_HOSTS: + raise KeyError("Please specify a list of allowed hosts in the configuration file.") + +CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] + + +# Synapse configuration + +if "synapse" not in config: + raise KeyError("Please specify a Synapse configuration in the configuration file.") + +if not all(key in config["synapse"] for key in ["server", "admin_token", "domain"]): + raise KeyError( + "Please specify the Synapse server URL, admin token, and domain in the configuration file." + ) + +SYNAPSE_SERVER = config["synapse"]["server"] +SYNAPSE_ADMIN_TOKEN = config["synapse"]["admin_token"] +MATRIX_DOMAIN = config["synapse"]["domain"] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "synapse_registration.registration", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "synapse_registration.synapse_registration.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "synapse_registration.synapse_registration.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Email configuration + +if "email" not in config: + raise KeyError("Please specify an email configuration in the configuration file.") + +if not all(key in config["email"] for key in ["host", "port", "username", "password"]): + raise KeyError( + "Please specify the email host, port, username, and password in the configuration file." + ) + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = config["email"]["host"] +EMAIL_PORT = config["email"]["port"] +EMAIL_USE_TLS = config["email"].get("tls", False) +EMAIL_USE_SSL = config["email"].get("ssl", False) +EMAIL_HOST_USER = config["email"]["username"] +EMAIL_HOST_PASSWORD = config["email"]["password"] +EMAIL_SUBJECT_PREFIX = config["email"].get("subject_prefix", "") +DEFAULT_FROM_EMAIL = config["email"].get("from", EMAIL_HOST_USER) diff --git a/src/synapse_registration/synapse_registration/urls.py b/src/synapse_registration/synapse_registration/urls.py new file mode 100644 index 0000000..f7c9c03 --- /dev/null +++ b/src/synapse_registration/synapse_registration/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for synapse_registration project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('', include('synapse_registration.registration.urls')), + path('admin/', admin.site.urls), +] diff --git a/src/synapse_registration/synapse_registration/wsgi.py b/src/synapse_registration/synapse_registration/wsgi.py new file mode 100644 index 0000000..b4f9a47 --- /dev/null +++ b/src/synapse_registration/synapse_registration/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for synapse_registration project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'synapse_registration.settings') + +application = get_wsgi_application()