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:
|
||||
|
||||
* `imprint.md` with your imprint information.
|
||||
* `privacy_policy.md` with your privacy policy.
|
||||
|
||||
## 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",
|
||||
]
|
||||
|
||||
RELEASE_VERSION = "0.13"
|
||||
RELEASE_VERSION = "0.15"
|
||||
|
||||
# Import settings from configuration.py
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
h1 {
|
||||
font-size: 2rem
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.pixelart {
|
||||
|
|
@ -10,3 +12,7 @@ h1 {
|
|||
min-width: 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.urls import path, reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
from django import forms
|
||||
|
||||
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.
|
||||
admin.site.register(BorgRepository)
|
||||
admin.site.register(Voucher)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,7 @@ class Command(BaseCommand):
|
|||
parser.add_argument("amount", type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
unabmigous_characters = "abcdefghijkmnopqrstuvwxyzACDEFHJKLMNPQRTUVWXY1234679"
|
||||
|
||||
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)
|
||||
vouchers = Voucher.bulk_generate(options["prefix"], options["amount"])
|
||||
|
||||
header = r"""\begin{document}
|
||||
\begin{center}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from random import choices
|
||||
from pathlib import Path
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import RegexValidator
|
||||
from django.conf import settings
|
||||
from pathlib import Path
|
||||
import msgpack
|
||||
|
||||
|
||||
from .tasks import update_user
|
||||
|
||||
|
||||
|
|
@ -121,3 +122,20 @@ class Voucher(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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 %}
|
||||
|
|
@ -2,37 +2,15 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="{% static 'vendored/css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'vendored/css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'webui/css/webui.css' %}">
|
||||
<link rel="icon" href="{% static 'webui/logo.png'%}" type="image/png"/>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Community Backup</title>
|
||||
<script type="text/javascript">
|
||||
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>
|
||||
<script type="text/javascript" src={% static 'webui/js/webui.js' %}></script>
|
||||
</head>
|
||||
<body style="display: flex; flex-direction: column; height: 100vh">
|
||||
<body>
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation" >
|
||||
<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>
|
||||
|
|
@ -84,9 +62,16 @@
|
|||
{% endblock %}
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="columns">
|
||||
<div class="columns container">
|
||||
<div class="column">
|
||||
<a href="{% url 'imprint' %}">Imprint</a>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% url 'imprint' %}">Imprint</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'privacy_policy' %}">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="https://git.srvspace.net/jo/community-backup">Source Code</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="section content">
|
||||
<nav class="level">
|
||||
<div class="level-left">
|
||||
<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>
|
||||
</div>
|
||||
</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.
|
||||
<b>Always use encrypted backups!</b></p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<section class="section content">
|
||||
<table class="table is-striped is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
|
|
@ -28,11 +28,15 @@
|
|||
{% for repo in object_list %}
|
||||
<tr>
|
||||
<td>{{ repo.name }}</td>
|
||||
<td style="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.truncated_key }}</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><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></td>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -40,15 +44,15 @@
|
|||
</section>
|
||||
|
||||
{% if BORG_SERVER_PUBKEYS %}
|
||||
<section class="section">
|
||||
<section class="section content">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Borg Server Public Keys</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% for key, fingerprint in BORG_SERVER_PUBKEYS %}
|
||||
<span style="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>{{ key }}</code></span><br>
|
||||
<span class="word-break-break-all"><code>{{ fingerprint }}</code></span>
|
||||
{% if not forloop.last %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="has-text-centered">Welcome to Community Backup!</h1>
|
||||
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<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>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>
|
||||
<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>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>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<section class="section content">
|
||||
{{ markdown_content|safe }}
|
||||
</section >
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ urlpatterns = [
|
|||
path("register/", views.RegisterUserView.as_view(), name="register"),
|
||||
path("delete/", views.DeleteUserView.as_view(), name="delete"),
|
||||
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"
|
||||
|
||||
|
||||
class PrivacyPolicyView(MarkdownView):
|
||||
template_name = "markdown.html"
|
||||
md = "privacy_policy.md"
|
||||
|
||||
|
||||
class BorgView(LoginRequiredMixin, ListView):
|
||||
template_name = "borg_list.html"
|
||||
content_object_name = "borg_repositories"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "community_backup"
|
||||
version = "0.13"
|
||||
version = "0.15"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
|
|
|||
4
uv.lock
generated
4
uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
revision = 5
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
|
|
@ -13,7 +13,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "community-backup"
|
||||
version = "0.13"
|
||||
version = "0.15"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "crispy-bulma" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue