Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49108672c8 | |||
| 6013d0bcab | |||
| 26f3d5d033 | |||
| 5813d200dc | |||
| 5471b20f7b | |||
| baea657d1a | |||
| f3d152b454 | |||
| 5c05ec0a8b | |||
| d7df062509 | |||
| ceafcf5895 |
17 changed files with 165 additions and 61 deletions
11
README.md
11
README.md
|
|
@ -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';"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
24
community_backup/static/webui/js/webui.js
Normal file
24
community_backup/static/webui/js/webui.js
Normal 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');
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="submit">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
4
uv.lock
generated
|
|
@ -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" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue