From 8b46b747deeb1411ca6033d59154a3d8b04fd953 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 20:30:55 +0200 Subject: [PATCH 1/2] Fix typo in landing page --- community_backup/webui/templates/landing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community_backup/webui/templates/landing.html b/community_backup/webui/templates/landing.html index b7a8141..a9960f9 100644 --- a/community_backup/webui/templates/landing.html +++ b/community_backup/webui/templates/landing.html @@ -6,7 +6,7 @@ -

This is a service offering space for backups free of charge. This is a hobby project. It comes with no garantees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailable at times and features might change quickly.

+

This is a service offering space for backups free of charge. This is a hobby project. It comes with no guarantees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailable at times and features might change quickly.

Currently this service offers only backups via Borg Backup.

To access this service you need an account. To register for an account you need a voucher. Vouchers are required to control the amount of users, so that there is enough space available for everyone.

From 02fad76482e5e18032a22680133028f8b1ebc264 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 20:34:07 +0200 Subject: [PATCH 2/2] reworked SSH Key handling --- community_backup/tests/__init__.py | 0 community_backup/tests/test_models.py | 54 +++++++++++++++++++ community_backup/webui/deployments.py | 4 +- community_backup/webui/forms.py | 2 +- community_backup/webui/models.py | 45 ++++++++++++++-- .../webui/templates/borg_list.html | 2 +- 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 community_backup/tests/__init__.py create mode 100644 community_backup/tests/test_models.py diff --git a/community_backup/tests/__init__.py b/community_backup/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community_backup/tests/test_models.py b/community_backup/tests/test_models.py new file mode 100644 index 0000000..74e2aa4 --- /dev/null +++ b/community_backup/tests/test_models.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from webui.models import BorgRepository + + +class BorgRepositoryTestCase(TestCase): + def test_ssh_key_regex(self): + user = User.objects.create(username="foo") + + # good repo + repo = BorgRepository.objects.create( + name="test1", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + + repo.full_clean() + + # good repo, key without comment + repo = BorgRepository.objects.create( + name="test2", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3", + user=user, + ) + repo.full_clean() + + # bad repo (wrong key) + repo = BorgRepository.objects.create(name="test3", key="Foo", user=user) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo (multiple valid keys ) + repo = BorgRepository.objects.create( + name="test4", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo, leading whitespace + repo = BorgRepository.objects.create( + name="test5", + key=" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # good repo, with whitespace in commment + repo = BorgRepository.objects.create( + name="test6", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris whitespace in comment", + user=user, + ) + repo.full_clean() diff --git a/community_backup/webui/deployments.py b/community_backup/webui/deployments.py index b56d1bc..527dda3 100644 --- a/community_backup/webui/deployments.py +++ b/community_backup/webui/deployments.py @@ -8,8 +8,6 @@ from django.conf import settings logger = getLogger("deployments") -# sync users - def sync_repos(dry_run=False): """Synchronize the repos""" @@ -19,7 +17,7 @@ def sync_repos(dry_run=False): repos_by_key = defaultdict(list) for repo in repos: - repos_by_key[repo.key].append(repo) + repos_by_key[f"{repo.key_type} {repo.key_value}"].append(repo) # create .ssh directory ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent diff --git a/community_backup/webui/forms.py b/community_backup/webui/forms.py index 99e774c..3ac0cb0 100644 --- a/community_backup/webui/forms.py +++ b/community_backup/webui/forms.py @@ -10,7 +10,7 @@ class BorgRepositoryForm(forms.ModelForm): class Meta: fields = ["name", "key", "user"] model = BorgRepository - widgets = {"user": forms.HiddenInput()} + widgets = {"user": forms.HiddenInput(), "key": forms.TextInput()} class RegisterUserForm(UserCreationForm): diff --git a/community_backup/webui/models.py b/community_backup/webui/models.py index 75c1962..d04785a 100644 --- a/community_backup/webui/models.py +++ b/community_backup/webui/models.py @@ -21,7 +21,7 @@ class BorgRepository(models.Model): key = models.TextField( validators=[ RegexValidator( - r"(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+) (\S*)", + r"^(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+)( ([\S ]*))?$", message="not a valid SSH public key.", ), ] @@ -46,9 +46,9 @@ class BorgRepository(models.Model): def repo_url(self) -> str: return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}" - def save(self): + def save(self, *args, **kwargs): - super().save() + super().save(*args, **kwargs) update_user.enqueue() def delete(self): @@ -56,6 +56,45 @@ class BorgRepository(models.Model): super().delete() update_user.enqueue() + @property + def key_type(self) -> str: + return self.key.split(" ", 1)[0].strip() + + @property + def key_value(self) -> str: + return self.key.split(" ", 2)[1].strip() + + @property + def key_comment(self) -> str | None: + splits = self.key.split(" ", 2) + if len(splits) == 3: + return splits[2].strip() + return None + + def truncated_key(self, length: int = 60): + """ + Returns a truncated version of the key for display purposes. + The Key type and the comment are retained as much as possible. + """ + + # check how long the truncated part of the key may be at most. + max_len = length - len(self.key_type) - 1 + if self.key_comment: + max_len = max_len - len(self.key_comment) - 1 + + if len(self.key_value) > max_len: + max_len -= 1 # remove one character if we add an ellipsis + trunc_key = self.key_value[:max_len] + "…" + else: + trunc_key = self.key_value + + output = f"{self.key_type} {trunc_key}" + + if self.key_comment: + output += f" {self.key_comment}" + + return output + class Voucher(models.Model): used = models.BooleanField(default=False) diff --git a/community_backup/webui/templates/borg_list.html b/community_backup/webui/templates/borg_list.html index b193fcd..cd508d4 100644 --- a/community_backup/webui/templates/borg_list.html +++ b/community_backup/webui/templates/borg_list.html @@ -24,7 +24,7 @@ {% for repo in object_list %} {{ repo.name }} - {{ repo.key | truncatechars:40 }} + {{ repo.truncated_key }} {{ repo.repo_url}} Edit Delete