Compare commits

..

10 commits
0.13 ... main

17 changed files with 165 additions and 61 deletions

View file

@ -95,6 +95,7 @@ Those files are markdown files that are rendered as HTML. You have to supply the
The following files are expected: The following files are expected:
* `imprint.md` with your imprint information. * `imprint.md` with your imprint information.
* `privacy_policy.md` with your privacy policy.
## Settings ## Settings
@ -176,3 +177,13 @@ BORG_SERVER_PUBKEYS = [
), ),
] ]
``` ```
## Webserver Config / Content-Security-Policy
This page by itself does not load external sources and does not use inline javascript/styling.
It only loads javascript, CSS and images.
Therefore the following Content-Security-Policy should work fine in the standard cases:
```
Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self';"
```

View file

@ -120,7 +120,7 @@ STATICFILES_DIRS = [
BASE_DIR / "static", BASE_DIR / "static",
] ]
RELEASE_VERSION = "0.13" RELEASE_VERSION = "0.15"
# Import settings from configuration.py # Import settings from configuration.py
try: try:

View file

@ -1,5 +1,7 @@
h1 { body {
font-size: 2rem display: flex;
flex-direction: column;
height: 100vh;
} }
.pixelart { .pixelart {
@ -10,3 +12,7 @@ h1 {
min-width: 32px; min-width: 32px;
min-height: 32px; min-height: 32px;
} }
.word-break-break-all {
word-break: break-all;
}

View file

@ -0,0 +1,24 @@
//
// Activation script for the bulma burger menu.
// Copied from https://bulma.io/documentation/components/navbar/
//
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});

View file

@ -1,7 +1,50 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, reverse_lazy
from django.views.generic import FormView
from django import forms
from .models import BorgRepository, Voucher from .models import BorgRepository, Voucher
class VoucherBulkCreateForm(forms.Form):
prefix = forms.CharField(label="Voucher prefix")
amount = forms.IntegerField(label="Amount")
class VoucherBulkCreateView(FormView):
form_class = VoucherBulkCreateForm
template_name = "admin/bulk_generate.html"
admin_site_context = None
success_url = reverse_lazy("admin:webui_voucher_changelist")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# inject the admin site context so that the navbar and other things work as intended
context.update(self.admin_site_context.each_context(self.request))
return context
def form_valid(self, form):
Voucher.bulk_generate(form.cleaned_data["prefix"], form.cleaned_data["amount"])
return super().form_valid(form)
@admin.register(Voucher)
class VoucherAdmin(admin.ModelAdmin):
change_list_template = "admin/voucher_change_list.html"
def get_urls(self):
urls = super().get_urls()
my_urls = [
path(
"bulk_generate/",
self.admin_site.admin_view(
VoucherBulkCreateView.as_view(admin_site_context=self.admin_site)
),
name="voucher_bulk_generate",
)
]
return my_urls + urls
# Register your models here. # Register your models here.
admin.site.register(BorgRepository) admin.site.register(BorgRepository)
admin.site.register(Voucher)

View file

@ -11,14 +11,7 @@ class Command(BaseCommand):
parser.add_argument("amount", type=int) parser.add_argument("amount", type=int)
def handle(self, *args, **options): def handle(self, *args, **options):
unabmigous_characters = "abcdefghijkmnopqrstuvwxyzACDEFHJKLMNPQRTUVWXY1234679" vouchers = Voucher.bulk_generate(options["prefix"], options["amount"])
vouchers = set()
while len(vouchers) < options["amount"]:
random_part = "".join(choices(unabmigous_characters, k=5))
vouchers.add(f"{options['prefix']}-{random_part}")
Voucher.objects.bulk_create(Voucher(code=v) for v in vouchers)
header = r"""\begin{document} header = r"""\begin{document}
\begin{center} \begin{center}

View file

@ -1,11 +1,12 @@
from random import choices
from pathlib import Path
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.conf import settings from django.conf import settings
from pathlib import Path
import msgpack import msgpack
from .tasks import update_user from .tasks import update_user
@ -121,3 +122,20 @@ class Voucher(models.Model):
def __str__(self): def __str__(self):
return f"Voucher '{self.code}'" return f"Voucher '{self.code}'"
@classmethod
def bulk_generate(cls, prefix: str, amount: int) -> set[str]:
"""
Create `amount` vouchers starting with `prefix`.
Returns a set of the voucher codes as strings.
"""
unabmigous_characters = "abcdefghijkmnopqrstuvwxyzACDEFHJKLMNPQRTUVWXY1234679"
vouchers = set()
while len(vouchers) < amount:
random_part = "".join(choices(unabmigous_characters, k=5))
vouchers.add(f"{prefix}-{random_part}")
Voucher.objects.bulk_create(Voucher(code=v) for v in vouchers)
return vouchers

View file

@ -0,0 +1,8 @@
{% extends "admin/base_site.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="submit">
</form>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "admin/change_list.html" %}
{% block object-tools-items %}
{{ block.super }}
<li><a href="{% url 'admin:voucher_bulk_generate' %}" class="addlink">Bulk Generate Vouchers</a></li>
{% endblock %}

View file

@ -8,31 +8,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Community Backup</title> <title>Community Backup</title>
<script type="text/javascript"> <script type="text/javascript" src={% static 'webui/js/webui.js' %}></script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</head> </head>
<body style="display: flex; flex-direction: column; height: 100vh"> <body>
<nav class="navbar" role="navigation" aria-label="main navigation" > <nav class="navbar" role="navigation" aria-label="main navigation" >
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"><img id="logo" class="pixelart" src="{% static 'webui/logo.png' %}" alt="The logo if Community Backup, a pixelated hard disk platter and head."/> Community Backup</a> <a class="navbar-item" href="/"><img id="logo" class="pixelart" src="{% static 'webui/logo.png' %}" alt="The logo if Community Backup, a pixelated hard disk platter and head."/> Community Backup</a>
@ -84,9 +62,16 @@
{% endblock %} {% endblock %}
</main> </main>
<footer class="footer"> <footer class="footer">
<div class="columns"> <div class="columns container">
<div class="column"> <div class="column">
<ul>
<li>
<a href="{% url 'imprint' %}">Imprint</a> <a href="{% url 'imprint' %}">Imprint</a>
</li>
<li>
<a href="{% url 'privacy_policy' %}">Privacy Policy</a>
</li>
</ul>
</div> </div>
<div class="column"> <div class="column">
<a href="https://git.srvspace.net/jo/community-backup">Source Code</a> <a href="https://git.srvspace.net/jo/community-backup">Source Code</a>

View file

@ -1,6 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<section class="section content">
<nav class="level"> <nav class="level">
<div class="level-left"> <div class="level-left">
<h1 class="level-item">Your Borg Repositories</h1> <h1 class="level-item">Your Borg Repositories</h1>
@ -9,13 +11,11 @@
<a class="level-item button is-primary" href="{% url 'borg_add' %}" role="button">Add Repository</a> <a class="level-item button is-primary" href="{% url 'borg_add' %}" role="button">Add Repository</a>
</div> </div>
</nav> </nav>
<section class="section">
<p>Create a borg repository by specifying a name and the SSH public key that is allowed to access this repository. The name is only for your convenience. To set up the repository please follow the <a href="https://borgbackup.readthedocs.io/en/stable/">BorgBackup documentation</a>. There is also a <a href="https://borgbackup.readthedocs.io/en/stable/quickstart.html">Quick start guide</a> available. <p>Create a borg repository by specifying a name and the SSH public key that is allowed to access this repository. The name is only for your convenience. To set up the repository please follow the <a href="https://borgbackup.readthedocs.io/en/stable/">BorgBackup documentation</a>. There is also a <a href="https://borgbackup.readthedocs.io/en/stable/quickstart.html">Quick start guide</a> available.
<b>Always use encrypted backups!</b></p> <b>Always use encrypted backups!</b></p>
</section> </section>
<section class="section"> <section class="section content">
<table class="table is-striped is-fullwidth is-hoverable"> <table class="table is-striped is-fullwidth is-hoverable">
<thead> <thead>
<th>Name</th> <th>Name</th>
@ -28,11 +28,15 @@
{% for repo in object_list %} {% for repo in object_list %}
<tr> <tr>
<td>{{ repo.name }}</td> <td>{{ repo.name }}</td>
<td style="word-break: break-all"><code>{{ repo.truncated_key }}</code></td> <td class="word-break-break-all"><code>{{ repo.truncated_key }}</code></td>
<td style="word-break: break-all"><code>{{ repo.repo_url }}</code></td> <td class="word-break-break-all"><code>{{ repo.repo_url }}</code></td>
<td>{% if repo.used_quota < 0 %}not initialized{% else %}{{ repo.used_quota }}/{{ repo.quota }} GB{% endif %}</td> <td>{% if repo.used_quota < 0 %}not initialized{% else %}{{ repo.used_quota }}/{{ repo.quota }} GB{% endif %}</td>
<td><a href="{% url 'borg_update' pk=repo.pk %}" class="button is-warning is-small">Edit</a> <td>
<a href="{% url 'borg_delete' pk=repo.pk %}" class="button is-danger is-small">Delete</a></td> <div class="field is-grouped">
<a href="{% url 'borg_update' pk=repo.pk %}" class="button is-warning is-small">Edit</a>
<a href="{% url 'borg_delete' pk=repo.pk %}" class="button is-danger is-small">Delete</a>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -40,15 +44,15 @@
</section> </section>
{% if BORG_SERVER_PUBKEYS %} {% if BORG_SERVER_PUBKEYS %}
<section class="section"> <section class="section content">
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Borg Server Public Keys</p> <p class="card-header-title">Borg Server Public Keys</p>
</header> </header>
<div class="card-content"> <div class="card-content">
{% for key, fingerprint in BORG_SERVER_PUBKEYS %} {% for key, fingerprint in BORG_SERVER_PUBKEYS %}
<span style="word-break: break-all"><code>{{ key }}</code></span><br> <span class="word-break-break-all"><code>{{ key }}</code></span><br>
<span style="word-break: break-all"><code>{{ fingerprint }}</code></span> <span class="word-break-break-all"><code>{{ fingerprint }}</code></span>
{% if not forloop.last %} {% if not forloop.last %}
<hr> <hr>
{% endif %} {% endif %}

View file

@ -1,12 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="content">
<h1 class="has-text-centered">Welcome to Community Backup!</h1>
<section class="section"> <section class="section">
<h1 class="has-text-centered">Welcome to Community Backup!</h1>
<p>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.</p> <p>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.</p>
<p>Currently this service offers backups only via <a href="https://www.borgbackup.org/">Borg Backup</a>.</p> <p>Currently this service offers backups only via <a href="https://www.borgbackup.org/">Borg Backup</a>.</p>
<p>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.</p> <p>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.</p>
</section> </section>
</div>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<section class="section"> <section class="section content">
{{ markdown_content|safe }} {{ markdown_content|safe }}
</section > </section >
{% endblock %} {% endblock %}

View file

@ -19,4 +19,5 @@ urlpatterns = [
path("register/", views.RegisterUserView.as_view(), name="register"), path("register/", views.RegisterUserView.as_view(), name="register"),
path("delete/", views.DeleteUserView.as_view(), name="delete"), path("delete/", views.DeleteUserView.as_view(), name="delete"),
path("imprint/", views.ImprintView.as_view(), name="imprint"), path("imprint/", views.ImprintView.as_view(), name="imprint"),
path("privacy-policy/", views.PrivacyPolicyView.as_view(), name="privacy_policy"),
] ]

View file

@ -51,6 +51,11 @@ class ImprintView(MarkdownView):
md = "imprint.md" md = "imprint.md"
class PrivacyPolicyView(MarkdownView):
template_name = "markdown.html"
md = "privacy_policy.md"
class BorgView(LoginRequiredMixin, ListView): class BorgView(LoginRequiredMixin, ListView):
template_name = "borg_list.html" template_name = "borg_list.html"
content_object_name = "borg_repositories" content_object_name = "borg_repositories"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "community_backup" name = "community_backup"
version = "0.13" version = "0.15"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"

4
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1 version = 1
revision = 3 revision = 5
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]] [[package]]
@ -13,7 +13,7 @@ wheels = [
[[package]] [[package]]
name = "community-backup" name = "community-backup"
version = "0.13" version = "0.15"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "crispy-bulma" }, { name = "crispy-bulma" },