Domains management pages

This commit is contained in:
Andrew Godwin 2022-11-16 21:12:28 -07:00
parent 44af0d4c59
commit 1b52acdb56
16 changed files with 308 additions and 12 deletions

View File

@ -43,7 +43,6 @@ the less sure I am about it.
- [ ] Receive post edits
- [x] Set content warnings on posts
- [x] Show content warnings on posts
- [ ] Attach images to posts
- [ ] Receive images on posts
- [x] Create boosts
- [x] Receive boosts
@ -65,8 +64,8 @@ the less sure I am about it.
- [x] Multiple domain support
- [x] Multiple identity support
- [x] Serverless-friendly worker subsystem
- [ ] Settings subsystem
- [ ] Server management page
- [x] Settings subsystem
- [x] Server management page
- [ ] Domain management page
- [ ] Email subsystem
- [ ] Signup flow
@ -75,6 +74,7 @@ the less sure I am about it.
### Beta
- [ ] Attach images to posts
- [ ] Delete posts
- [ ] Reply threading on post creation
- [ ] Display posts with reply threads

View File

@ -108,6 +108,7 @@ class Boost(View):
class Compose(FormView):
template_name = "activities/compose.html"
extra_context = {"top_section": "compose"}
class form_class(forms.Form):
text = forms.CharField(

View File

@ -59,7 +59,7 @@ class HttpSignature:
elif header_name == "content-type":
value = request.META["CONTENT_TYPE"]
else:
value = request.META[f"HTTP_{header_name.upper()}"]
value = request.META["HTTP_%s" % header_name.upper().replace("-", "_")]
headers[header_name] = value
return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items())

View File

@ -81,6 +81,7 @@ a {
--color-bg-box: #1a2631;
--color-bg-error: rgb(87, 32, 32);
--color-highlight: #449c8c;
--color-delete: #8b2821;
--color-text-duller: #5f6983;
--color-text-dull: #99a;
@ -148,7 +149,8 @@ header menu a {
border-right: 1px solid var(--color-bg-menu);
}
header menu a:hover {
header menu a:hover,
header menu a.selected {
border-bottom: 3px solid var(--color-highlight);
}
@ -438,6 +440,11 @@ form .button {
display: inline-block;
}
form button.delete,
form .button.delete {
background: var(--color-delete);
}
form button.toggle,
form .button.toggle {
background: var(--color-bg-main);

View File

@ -17,6 +17,13 @@ urlpatterns = [
path("settings/interface/", settings_identity.InterfacePage.as_view()),
path("settings/system/", settings_system.SystemSettingsRoot.as_view()),
path("settings/system/basic/", settings_system.BasicPage.as_view()),
path("settings/system/domains/", settings_system.DomainsPage.as_view()),
path("settings/system/domains/create/", settings_system.DomainCreatePage.as_view()),
path("settings/system/domains/<domain>/", settings_system.DomainEditPage.as_view()),
path(
"settings/system/domains/<domain>/delete/",
settings_system.DomainDeletePage.as_view(),
),
# Identity views
path("@<handle>/", identity.ViewIdentity.as_view()),
path("@<handle>/actor/", activitypub.Actor.as_view()),

View File

@ -28,10 +28,16 @@
</a>
<menu>
{% if user.is_authenticated %}
<a href="/compose/" title="Compose"><i class="fa-solid fa-feather"></i> Compose</a>
<a href="/settings/" title="Settings"><i class="fa-solid fa-gear"></i> Settings</a>
<a href="/compose/" title="Compose" {% if top_section == "compose" %}class="selected"{% endif %}>
<i class="fa-solid fa-feather"></i> Compose
</a>
<a href="/settings/" title="Settings" {% if top_section == "settings" %}class="selected"{% endif %}>
<i class="fa-solid fa-gear"></i> Settings
</a>
{% if request.user.admin %}
<a href="/settings/system/" title="Admin"><i class="fa-solid fa-toolbox"></i> Admin</a>
<a href="/settings/system/" title="Admin" {% if top_section == "settings_system" %}class="selected"{% endif %}>
<i class="fa-solid fa-toolbox"></i> Admin
</a>
{% endif %}
<div class="gap"></div>
<a href="/identity/select/" class="identity">

View File

@ -5,7 +5,7 @@
</label>
{% if field.help_text %}
<p class="help">
{{ field.help_text }}
{{ field.help_text|linebreaksbr }}
</p>
{% endif %}
{{ field.errors }}

View File

@ -1,3 +1,5 @@
<nav>
<a href="#" {% if section == "basic" %}class="selected"{% endif %}>Basic</a>
<a href="/settings/system/basic/" {% if section == "basic" %}class="selected"{% endif %}>Basic</a>
<a href="/settings/system/domains/" {% if section == "domains" %}class="selected"{% endif %}>Domains</a>
<a href="/settings/system/users/" {% if section == "users" %}class="selected"{% endif %}>Users</a>
</nav>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Add Domain - System Settings{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% endblock %}
<form action="." method="POST">
<h1>Add A Domain</h1>
<p>
Use this form to add a domain that your users can create identities
on.
</p>
<p>
Takahē supports multiple domains per server, but note that when
identities are created they are <b>fixed to their chosen domain</b>,
and you will <b>not be able to delete a domain with identities on it</b>.
</p>
<p>
If you will be serving Takahē on the domain you choose, you can leave
the "service domain" field blank. If you would like to let users create
accounts on a domain serving something else, you must pick a unique
"service domain" that pairs up to your chosen domain name, make sure
Takahē is served on that, and add redirects
for <tt>/.well-known/webfinger</tt>, <tt>/.well-known/host-meta</tt>
and <tt>/.well-known/nodeinfo</tt> from the main domain to the
service domain.
</p>
{% csrf_token %}
{% for field in form %}
{% include "forms/_field.html" %}
{% endfor %}
<div class="buttons">
<a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
<button>Save</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}Delete {{ domain.domain }} - System Settings{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% endblock %}
<form action="." method="POST">
{% csrf_token %}
<h1>Deleting {{ domain.domain }}</h1>
{% if num_identities %}
<p>
You cannot delete this domain as it has <b>{{ num_identities }}
identit{{ num_identities|pluralize:"y,ies" }}</b> registered on it.
</p>
<p>
You will need to manually remove all identities from this domain in
order to delete it.
</p>
{% else %}
<p>Please confirm deletion of this domain - there are no identities registed on it.</p>
<div class="buttons">
<a class="button" href="{{ domain.urls.edit }}">Cancel</a>
<button class="delete">Confirm Deletion</button>
</div>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}{{ domain.domain }} - System Settings{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% endblock %}
<form action="." method="POST">
{% csrf_token %}
{% for field in form %}
{% include "forms/_field.html" %}
{% endfor %}
<div class="buttons">
<a href="{{ domain.urls.delete }}" class="button delete">Delete</a>
<button>Save</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}{{ section.title }} - System Settings{% endblock %}
{% block content %}
{% block menu %}
{% include "settings/_settings_system_menu.html" %}
{% endblock %}
<section class="icon-menu">
{% for domain in domains %}
<a class="option" href="{{ domain.urls.edit }}">
<i class="fa-solid fa-globe"></i>
<span class="handle">
{{ domain.domain }}
<small>
{% if domain.public %}Public{% else %}Private{% endif %}
{% if domain.service_domain %}({{ domain.service_domain }}){% endif %}
</small>
</span>
</a>
{% empty %}
<p class="option empty">You have no domains set up.</p>
{% endfor %}
<a href="/settings/system/domains/create/" class="option new">
<i class="fa-solid fa-plus"></i> Add a domain
</a>
</section>
{% endblock %}

View File

@ -1,5 +1,6 @@
from typing import Optional
import urlman
from django.db import models
@ -47,6 +48,12 @@ class Domain(models.Model):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class urls(urlman.Urls):
root = "/settings/system/domains/"
create = "/settings/system/domains/create/"
edit = "/settings/system/domains/{self.domain}/"
delete = "/settings/system/domains/{self.domain}/delete/"
@classmethod
def get_remote_domain(cls, domain: str) -> "Domain":
return cls.objects.get_or_create(domain=domain, local=False)[0]

View File

@ -67,6 +67,7 @@ class Identity(StatorModel):
blank=True,
null=True,
on_delete=models.PROTECT,
related_name="identities",
)
name = models.CharField(max_length=500, blank=True, null=True)

View File

@ -17,6 +17,8 @@ class IdentitySettingsPage(SystemSettingsPage):
at the bottom of the page. Don't add this to a URL directly - subclass!
"""
extra_context = {"top_section": "settings"}
options_class = Config.IdentityOptions
template_name = "settings/settings_identity.html"

View File

@ -1,13 +1,16 @@
import re
from functools import partial
from typing import ClassVar, Dict
from django import forms
from django.shortcuts import redirect
from django.db import models
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.views.generic import FormView, RedirectView
from django.views.generic import FormView, RedirectView, TemplateView
from core.models import Config
from users.decorators import identity_required
from users.models import Domain
@method_decorator(identity_required, name="dispatch")
@ -27,6 +30,8 @@ class SystemSettingsPage(FormView):
section: ClassVar[str]
options: Dict[str, Dict[str, str]]
extra_context = {"top_section": "settings_system"}
def get_form_class(self):
# Create the fields dict from the config object
fields = {}
@ -93,3 +98,142 @@ class BasicPage(SystemSettingsPage):
"help_text": "Used for logo background and other highlights",
},
}
class DomainsPage(TemplateView):
template_name = "settings/settings_system_domains.html"
def get_context_data(self):
return {
"domains": Domain.objects.filter(local=True).order_by("domain"),
"section": "domains",
}
class DomainCreatePage(FormView):
template_name = "settings/settings_system_domain_create.html"
extra_context = {"section": "domains"}
class form_class(forms.Form):
domain = forms.CharField(
help_text="The domain displayed as part of a user's identity.\nCannot be changed after the domain has been created.",
)
service_domain = forms.CharField(
help_text="Optional - a domain that serves Takahē if it is not running on the main domain.\nCannot be changed after the domain has been created.",
required=False,
)
public = forms.BooleanField(
help_text="If any user on this server can create identities here",
widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
required=False,
)
domain_regex = re.compile(
r"^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$"
)
def clean_domain(self):
if not self.domain_regex.match(self.cleaned_data["domain"]):
raise forms.ValidationError("This does not look like a domain name")
if Domain.objects.filter(
models.Q(domain=self.cleaned_data["domain"])
| models.Q(service_domain=self.cleaned_data["domain"])
):
raise forms.ValidationError("This domain name is already in use")
return self.cleaned_data["domain"]
def clean_service_domain(self):
if not self.cleaned_data["service_domain"]:
return None
if not self.domain_regex.match(self.cleaned_data["service_domain"]):
raise forms.ValidationError("This does not look like a domain name")
if Domain.objects.filter(
models.Q(domain=self.cleaned_data["service_domain"])
| models.Q(service_domain=self.cleaned_data["service_domain"])
):
raise forms.ValidationError("This domain name is already in use")
if self.cleaned_data.get("domain") == self.cleaned_data["service_domain"]:
raise forms.ValidationError(
"You cannot have the domain and service domain be the same (did you mean to leave service domain blank?)"
)
return self.cleaned_data["service_domain"]
def form_valid(self, form):
Domain.objects.create(
domain=form.cleaned_data["domain"],
service_domain=form.cleaned_data["service_domain"] or None,
public=form.cleaned_data["public"],
local=True,
)
return redirect(Domain.urls.root)
class DomainEditPage(FormView):
template_name = "settings/settings_system_domain_edit.html"
extra_context = {"section": "domains"}
class form_class(forms.Form):
domain = forms.CharField(
help_text="The domain displayed as part of a user's identity.\nCannot be changed after the domain has been created.",
disabled=True,
)
service_domain = forms.CharField(
help_text="Optional - a domain that serves Takahē if it is not running on the main domain.\nCannot be changed after the domain has been created.",
disabled=True,
required=False,
)
public = forms.BooleanField(
help_text="If any user on this server can create identities here",
widget=forms.Select(choices=[(True, "Public"), (False, "Private")]),
required=False,
)
def dispatch(self, request, domain):
self.domain = get_object_or_404(
Domain.objects.filter(local=True), domain=domain
)
return super().dispatch(request)
def get_context_data(self):
context = super().get_context_data()
context["domain"] = self.domain
return context
def form_valid(self, form):
self.domain.public = form.cleaned_data["public"]
self.domain.save()
return redirect(Domain.urls.root)
def get_initial(self):
return {
"domain": self.domain.domain,
"service_domain": self.domain.service_domain,
"public": self.domain.public,
}
class DomainDeletePage(TemplateView):
template_name = "settings/settings_system_domain_delete.html"
def dispatch(self, request, domain):
self.domain = get_object_or_404(
Domain.objects.filter(public=True), domain=domain
)
return super().dispatch(request)
def get_context_data(self):
return {
"domain": self.domain,
"num_identities": self.domain.identities.count(),
"section": "domains",
}
def post(self, request):
if self.domain.identities.exists():
raise ValueError("Tried to delete domain with identities!")
self.domain.delete()
return redirect("/settings/system/domains/")