summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--activities/views/posts.py1
-rw-r--r--core/signatures.py2
-rw-r--r--static/css/style.css9
-rw-r--r--takahe/urls.py7
-rw-r--r--templates/base.html12
-rw-r--r--templates/forms/_field.html2
-rw-r--r--templates/settings/_settings_system_menu.html4
-rw-r--r--templates/settings/settings_system_domain_create.html39
-rw-r--r--templates/settings/settings_system_domain_delete.html33
-rw-r--r--templates/settings/settings_system_domain_edit.html19
-rw-r--r--templates/settings/settings_system_domains.html28
-rw-r--r--users/models/domain.py7
-rw-r--r--users/models/identity.py1
-rw-r--r--users/views/settings_identity.py2
-rw-r--r--users/views/settings_system.py148
16 files changed, 308 insertions, 12 deletions
diff --git a/README.md b/README.md
index db94116..5ee9c32 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/activities/views/posts.py b/activities/views/posts.py
index 3ee35cc..d0ad813 100644
--- a/activities/views/posts.py
+++ b/activities/views/posts.py
@@ -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(
diff --git a/core/signatures.py b/core/signatures.py
index 8b52c1a..d981f87 100644
--- a/core/signatures.py
+++ b/core/signatures.py
@@ -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())
diff --git a/static/css/style.css b/static/css/style.css
index 01f9076..9c45eb3 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -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);
diff --git a/takahe/urls.py b/takahe/urls.py
index bdb5946..0643440 100644
--- a/takahe/urls.py
+++ b/takahe/urls.py
@@ -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()),
diff --git a/templates/base.html b/templates/base.html
index e392cb9..402dcd3 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -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">
diff --git a/templates/forms/_field.html b/templates/forms/_field.html
index 120724e..740432d 100644
--- a/templates/forms/_field.html
+++ b/templates/forms/_field.html
@@ -5,7 +5,7 @@
</label>
{% if field.help_text %}
<p class="help">
- {{ field.help_text }}
+ {{ field.help_text|linebreaksbr }}
</p>
{% endif %}
{{ field.errors }}
diff --git a/templates/settings/_settings_system_menu.html b/templates/settings/_settings_system_menu.html
index fb4da02..9206045 100644
--- a/templates/settings/_settings_system_menu.html
+++ b/templates/settings/_settings_system_menu.html
@@ -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>
diff --git a/templates/settings/settings_system_domain_create.html b/templates/settings/settings_system_domain_create.html
new file mode 100644
index 0000000..54d3640
--- /dev/null
+++ b/templates/settings/settings_system_domain_create.html
@@ -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 %}
diff --git a/templates/settings/settings_system_domain_delete.html b/templates/settings/settings_system_domain_delete.html
new file mode 100644
index 0000000..220bbb9
--- /dev/null
+++ b/templates/settings/settings_system_domain_delete.html
@@ -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 %}
diff --git a/templates/settings/settings_system_domain_edit.html b/templates/settings/settings_system_domain_edit.html
new file mode 100644
index 0000000..c05d5d5
--- /dev/null
+++ b/templates/settings/settings_system_domain_edit.html
@@ -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 %}
diff --git a/templates/settings/settings_system_domains.html b/templates/settings/settings_system_domains.html
new file mode 100644
index 0000000..dccde65
--- /dev/null
+++ b/templates/settings/settings_system_domains.html
@@ -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 %}
diff --git a/users/models/domain.py b/users/models/domain.py
index d2b17e2..af0bbab 100644
--- a/users/models/domain.py
+++ b/users/models/domain.py
@@ -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]
diff --git a/users/models/identity.py b/users/models/identity.py
index 15caef4..d97f5f0 100644
--- a/users/models/identity.py
+++ b/users/models/identity.py
@@ -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)
diff --git a/users/views/settings_identity.py b/users/views/settings_identity.py
index 8c52f9e..f35928a 100644
--- a/users/views/settings_identity.py
+++ b/users/views/settings_identity.py
@@ -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"
diff --git a/users/views/settings_system.py b/users/views/settings_system.py
index 52ba349..bfd9fb7 100644
--- a/users/views/settings_system.py
+++ b/users/views/settings_system.py
@@ -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/")