This commit is contained in:
Jonas Heinrich 2025-04-16 12:24:27 +02:00
parent 9ab43375b6
commit 186d472141
19 changed files with 412 additions and 17 deletions

View file

@ -31,6 +31,7 @@
postInstall = oldAttrs.postInstall + ''
rm -r $out/${pkgs.python3.sitePackages}/froide_govplan/templates
ln -sf /var/lib/froide-govplan/templates $out/${pkgs.python3.sitePackages}/froide_govplan/templates
cp -r froide_govplan/static $out/${pkgs.python3.sitePackages}/froide_govplan/
'';
});
})
@ -45,8 +46,13 @@
cores = 4;
};
disabledModules = [ "services/web-apps/froide-govplan.nix" ];
imports = [ ./froide-govplan.nix ];
services.froide-govplan = {
enable = true;
package = pkgs.froide-govplan;
settings = {
DEBUG = lib.mkForce true;
CSRF_TRUSTED_ORIGINS = [ "http://localhost:8080" ];

242
froide-govplan.nix Normal file
View file

@ -0,0 +1,242 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.froide-govplan;
pythonFmt = pkgs.formats.pythonVars { };
settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
pkg = cfg.package.overridePythonAttrs (old: {
src = ./.;
postInstall =
old.postInstall
+ ''
ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
rm -r $out/${pkgs.python3.sitePackages}/froide_govplan/templates
ln -sf /var/lib/froide-govplan/templates $out/${pkgs.python3.sitePackages}/froide_govplan/templates
cp -r froide_govplan/static $out/${pkgs.python3.sitePackages}/froide_govplan/
'';
});
froide-govplan = pkgs.writeShellApplication {
name = "froide-govplan";
runtimeInputs = [ pkgs.coreutils ];
text = ''
SUDO="exec"
if [[ "$USER" != govplan ]]; then
SUDO="exec /run/wrappers/bin/sudo -u govplan"
fi
$SUDO env ${lib.getExe pkg} "$@"
'';
};
# Service hardening
defaultServiceConfig = {
# Secure the services
ReadWritePaths = [ cfg.dataDir ];
CacheDirectory = "froide-govplan";
CapabilityBoundingSet = "";
# ProtectClock adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @setuid @keyring"
];
UMask = "0066";
};
in
{
options.services.froide-govplan = {
enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
package = lib.mkPackageOption pkgs "froide-govplan" { };
hostName = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "FQDN for the froide-govplan instance.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/froide-govplan";
description = "Directory to store the Froide-Govplan server data.";
};
secretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the secret key.
'';
};
settings = lib.mkOption {
description = ''
Configuration options to set in `extra_settings.py`.
'';
default = { };
type = lib.types.submodule {
freeformType = pythonFmt.type;
options = {
ALLOWED_HOSTS = lib.mkOption {
type = with lib.types; listOf str;
default = [ "*" ];
description = ''
A list of valid fully-qualified domain names (FQDNs) and/or IP
addresses that can be used to reach the Froide-Govplan service.
'';
};
};
};
};
};
config = lib.mkIf cfg.enable {
services.froide-govplan = {
settings = {
STATIC_ROOT = "${cfg.dataDir}/static";
DEBUG = false;
DATABASES.default = {
ENGINE = "django.contrib.gis.db.backends.postgis";
NAME = "govplan";
USER = "govplan";
HOST = "/run/postgresql";
};
};
};
services.postgresql = {
enable = true;
ensureDatabases = [ "govplan" ];
ensureUsers = [
{
name = "govplan";
ensureDBOwnership = true;
}
];
extensions = ps: with ps; [ postgis ];
};
services.nginx = {
enable = lib.mkDefault true;
virtualHosts."${cfg.hostName}".locations = {
"/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
"/static/".alias = "${cfg.dataDir}/static/";
};
proxyTimeout = lib.mkDefault "120s";
};
systemd = {
services = {
postgresql.serviceConfig.ExecStartPost =
let
sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
CREATE EXTENSION IF NOT EXISTS postgis;
'';
in
[
''
${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
''
];
froide-govplan = {
description = "Gouvernment planer Govplan";
serviceConfig = defaultServiceConfig // {
WorkingDirectory = cfg.dataDir;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
User = "govplan";
Group = "govplan";
};
after = [
"postgresql.service"
"network.target"
"systemd-tmpfiles-setup.service"
];
wantedBy = [ "multi-user.target" ];
environment =
{
PYTHONPATH = pkg.pythonPath;
GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
}
// lib.optionalAttrs (cfg.secretKeyFile != null) {
SECRET_KEY_FILE = cfg.secretKeyFile;
};
preStart = ''
# Auto-migrate on first run or if the package has changed
versionFile="${cfg.dataDir}/src-version"
version=$(cat "$versionFile" 2>/dev/null || echo 0)
if [[ $version != ${pkg.version} ]]; then
${lib.getExe pkg} migrate --no-input
${lib.getExe pkg} collectstatic --no-input --clear
echo ${pkg.version} > "$versionFile"
fi
'';
script = ''
${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
--app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
project.asgi:application
'';
};
};
};
systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
environment.systemPackages = [ froide-govplan ];
users.users.govplan = {
home = "${cfg.dataDir}";
isSystemUser = true;
group = "govplan";
};
users.groups.govplan = { };
};
meta.maintainers = with lib.maintainers; [ onny ];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 403 KiB

Before After
Before After

View file

@ -0,0 +1,32 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="640.000000pt" height="640.000000pt" viewBox="0 0 640.000000 640.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,640.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M990 5042 c0 -10 668 -1325 1170 -2302 103 -201 249 -486 325 -635
76 -148 192 -377 259 -507 l121 -238 132 0 132 0 44 83 c25 45 109 208 187
362 78 154 176 344 216 421 41 78 74 144 74 146 0 3 -60 8 -132 11 -140 5
-238 28 -371 83 -327 137 -587 446 -678 805 -27 108 -37 337 -19 447 16 98 53
227 84 292 l25 51 -247 489 -247 489 -538 3 c-295 2 -537 2 -537 0z"/>
<path d="M3850 4880 c-44 -88 -80 -162 -80 -165 0 -3 15 -5 33 -5 117 0 387
-109 517 -209 66 -50 205 -189 251 -250 l26 -35 210 412 210 412 -544 0 -543
0 -80 -160z"/>
<path d="M3431 4609 c-74 -13 -218 -62 -293 -100 -273 -138 -477 -392 -557
-692 -149 -557 191 -1143 755 -1304 92 -26 112 -28 274 -28 161 0 183 3 275
29 100 28 241 88 293 124 l27 20 92 -87 c91 -86 92 -88 81 -117 -6 -17 -8 -43
-4 -59 11 -42 724 -757 783 -785 62 -29 136 -27 202 6 101 51 148 159 117 268
-16 53 -31 72 -210 259 -304 316 -528 535 -557 542 -15 4 -40 5 -55 3 -23 -2
-35 4 -58 32 -16 19 -56 63 -89 96 l-59 62 45 63 c243 344 254 817 26 1187
-99 161 -283 324 -457 404 -141 64 -222 81 -407 84 -93 1 -194 -2 -224 -7z
m397 -325 c251 -72 427 -242 515 -498 28 -84 31 -102 31 -226 1 -157 -13 -221
-74 -345 -48 -96 -107 -170 -198 -247 -144 -123 -292 -177 -488 -178 -208 0
-389 75 -539 225 -151 151 -220 321 -219 535 1 126 17 203 64 306 88 194 271
359 475 429 111 38 297 37 433 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,80 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

View file

@ -7,30 +7,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{% block title %}{% page_attribute "page_title" %} - {{ SITE_NAME }}{% endblock %}</title>
<script src="https://getbootstrap.com/docs/5.3/assets/js/color-modes.js"></script>
<script src="{% static 'froide_govplan/js/color-modes.js' %}"></script>
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.122.0">
<link rel="canonical" href="https://getbootstrap.com/docs/5.3/examples/starter-template/">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@docsearch/css@3">
<link rel="stylesheet" href="{% static 'froide_govplan/css/docsearch.css' %}">
<!-- Favicons -->
<link rel="apple-touch-icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/safari-pinned-tab.svg" color="#712cf9">
<link rel="icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/favicon.ico">
<meta name="theme-color" content="#712cf9">
<!-- Favicons -->
<link rel="apple-touch-icon" href="{% static 'froide_govplan/images/apple-touch-icon.png' %}" sizes="180x180">
<link rel="icon" href="{% static 'froide_govplan/images/favicon-32x32.png' %}" sizes="32x32" type="image/png">
<link rel="icon" href="{% static 'froide_govplan/images/favicon-16x16.png' %}" sizes="16x16" type="image/png">
<link rel="manifest" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/safari-pinned-tab.svg" color="#712cf9">
<link rel="icon" href="https://getbootstrap.com/docs/5.3/assets/img/favicons/favicon.ico">
<meta name="theme-color" content="#712cf9">
<script defer data-domain="verwaltungstracker.de" src="https://analytics.project-insanity.org/js/script.js"></script>
{% block header_font %}{% endblock %}
{% block css %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">
<link href="{% static 'froide_govplan/css/bootstrap.min.css' %}" rel="stylesheet" crossorigin="anonymous">
<link href="{% static 'froide_govplan/css/font-awesome.css' %}" rel="stylesheet">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
@ -139,9 +141,8 @@
{% block extra_footer %}{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script defer data-domain="verwaltungstracker.de" src="https://analytics.project-insanity.org/js/script.js"></script>
<script src="{% static 'froide_govplan/js/bootstrap.bundle.min.js' %}" crossorigin="anonymous"></script>
<script src="{% static 'froide_govplan/js/jquery-3.6.4.min.js' %}"></script>
<script lang="javascript">
const Ze = t => {
const e = t.dataset.confirm;

View file

@ -2,6 +2,7 @@
{% load cms_tags %}
{% load djangocms_alias_tags %}
{% load menu_tags %}
{% load static %}
{% block body %}
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
@ -81,9 +82,10 @@
<div class="col-lg-10 mx-auto p-4 py-md-5">
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<a href="/" class="d-flex align-items-center text-body-emphasis text-decoration-none text-reset">
<svg class="bi me-2" width="40" height="32">
<!-- <svg class="bi me-2" width="40" height="32">
<use xlink:href="#bootstrap" />
</svg>
</svg> -->
<img src="{% static 'froide_govplan/images/apple-touch-icon.png' %}" class="bi me-2" height="32">
<span class="fs-4">VerwaltungsTracker</span>
</a>
</header>

View file

@ -161,6 +161,10 @@ USE_TZ = True
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static",
]
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field