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.
This commit is contained in:
commit
b5c816e748
34 changed files with 862 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
/dist/
|
||||||
|
config.yaml
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2024 Private.coffee Team <support@private.coffee>
|
||||||
|
|
||||||
|
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.
|
0
README.md
Normal file
0
README.md
Normal file
14
config.dist.yaml
Normal file
14
config.dist.yaml
Normal file
|
@ -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
|
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
|
@ -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"
|
22
src/synapse_registration/manage.py
Executable file
22
src/synapse_registration/manage.py
Executable file
|
@ -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()
|
0
src/synapse_registration/registration/__init__.py
Normal file
0
src/synapse_registration/registration/__init__.py
Normal file
22
src/synapse_registration/registration/admin.py
Normal file
22
src/synapse_registration/registration/admin.py
Normal file
|
@ -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"
|
9
src/synapse_registration/registration/apps.py
Normal file
9
src/synapse_registration/registration/apps.py
Normal file
|
@ -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
|
46
src/synapse_registration/registration/forms.py
Normal file
46
src/synapse_registration/registration/forms.py
Normal file
|
@ -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.")
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
25
src/synapse_registration/registration/models.py
Normal file
25
src/synapse_registration/registration/models.py
Normal file
|
@ -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
|
29
src/synapse_registration/registration/signals.py
Normal file
29
src/synapse_registration/registration/signals.py
Normal file
|
@ -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
|
24
src/synapse_registration/registration/templates/base.html
Normal file
24
src/synapse_registration/registration/templates/base.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>
|
||||||
|
{% block title %}
|
||||||
|
Registration
|
||||||
|
{% endblock title %}
|
||||||
|
</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://nobsdelivr.private.coffee/npm/bulma@0.9.3/css/bulma.min.css">
|
||||||
|
{% block css %}
|
||||||
|
{% endblock css %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock content %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Error
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notification is-danger">
|
||||||
|
<button class="delete"></button>
|
||||||
|
An error has occurred. Please try again later or contact support.
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h1 class="title">Something Went Wrong</h1>
|
||||||
|
<p>
|
||||||
|
We're sorry, but an unexpected error has occurred. Please try reloading the page, or <a href="{% url 'landing_page' %}">return to the homepage</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Welcome to Matrix!
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="hero is-primary">
|
||||||
|
<div class="hero-body">
|
||||||
|
<p class="title">Welcome to Our Matrix Server</p>
|
||||||
|
<p class="subtitle">Join us to connect securely and communicate openly across the Matrix network.</p>
|
||||||
|
<a href="{% url 'check_username' %}" class="button is-link">Start Your Journey</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="title">Why Join Our Matrix Server?</h2>
|
||||||
|
<p>
|
||||||
|
Matrix is an open standard for secure, decentralized, and real-time communication. By joining our server, you gain access to:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Secure messaging and VoIP with end-to-end encryption.</li>
|
||||||
|
<li>Interoperability with other Matrix users and servers globally.</li>
|
||||||
|
<li>Community and direct chats with powerful, extensible features.</li>
|
||||||
|
<li>A platform that respects your privacy and data ownership.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Already Verified
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notification is-info">Your email is already verified. You can proceed with your registration.</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Complete Registration
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">Complete Your Registration</h1>
|
||||||
|
<form method="post" class="box">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
{{ form.password1.label_tag }}
|
||||||
|
<div class="control">{{ form.password1 }}</div>
|
||||||
|
<p class="help is-danger">{{ form.password1.errors }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
{{ form.password2.label_tag }}
|
||||||
|
<div class="control">{{ form.password2 }}</div>
|
||||||
|
<p class="help is-danger">{{ form.password2.errors }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
{{ form.registration_reason.label_tag }}
|
||||||
|
<div class="control">{{ form.registration_reason }}</div>
|
||||||
|
<p class="help is-danger">{{ form.registration_reason.errors }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-link">Complete Registration</button>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Enter Your Email
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">Enter Your Email</h1>
|
||||||
|
<form method="post" class="box">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
{{ form.email.label_tag }}
|
||||||
|
<div class="control">{{ form.email }}</div>
|
||||||
|
<p class="help is-danger">{{ form.email.errors }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-link">Verify Email</button>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Email Sent
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notification is-success">Thank you! A verification link has been sent to your email.</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Access Forbidden
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notification is-danger">
|
||||||
|
You cannot complete registration at this time. Please verify your email or check the registration status.
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Registration Submitted
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="notification is-warning">
|
||||||
|
Your registration is pending approval. We will notify you via email once it is approved.
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Choose Username
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">Choose a Username</h1>
|
||||||
|
<form method="post" class="box">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="field">
|
||||||
|
{{ form.username.label_tag }}
|
||||||
|
<div class="control">{{ form.username }}</div>
|
||||||
|
<p class="help is-danger">{{ form.username.errors }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="button is-link">Next</button>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
3
src/synapse_registration/registration/tests.py
Normal file
3
src/synapse_registration/registration/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
26
src/synapse_registration/registration/urls.py
Normal file
26
src/synapse_registration/registration/urls.py
Normal file
|
@ -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/<str:token>/",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
]
|
132
src/synapse_registration/registration/views.py
Normal file
132
src/synapse_registration/registration/views.py
Normal file
|
@ -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)
|
16
src/synapse_registration/synapse_registration/asgi.py
Normal file
16
src/synapse_registration/synapse_registration/asgi.py
Normal file
|
@ -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()
|
183
src/synapse_registration/synapse_registration/settings.py
Normal file
183
src/synapse_registration/synapse_registration/settings.py
Normal file
|
@ -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)
|
23
src/synapse_registration/synapse_registration/urls.py
Normal file
23
src/synapse_registration/synapse_registration/urls.py
Normal file
|
@ -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),
|
||||||
|
]
|
16
src/synapse_registration/synapse_registration/wsgi.py
Normal file
16
src/synapse_registration/synapse_registration/wsgi.py
Normal file
|
@ -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()
|
Loading…
Reference in a new issue