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:
Kumi 2024-11-16 15:22:46 +01:00
commit b5c816e748
Signed by: kumi
GPG key ID: ECBCC9082395383F
34 changed files with 862 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
venv/
.venv/
__pycache__/
*.pyc
/dist/
config.yaml
db.sqlite3
db.sqlite3-journal

17
.vscode/launch.json vendored Normal file
View 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
View 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
View file

14
config.dist.yaml Normal file
View 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
View 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"

View 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()

View 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"

View 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

View 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.")

View file

@ -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)),
],
),
]

View file

@ -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),
),
]

View 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

View 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

View 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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View 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",
),
]

View 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)

View 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()

View 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)

View 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),
]

View 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()