Initial Bitpoll Nix package and service
This commit is contained in:
commit
0b3e086c03
5 changed files with 898 additions and 0 deletions
231
README.md
Normal file
231
README.md
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
# Bitpoll Nix Package
|
||||||
|
|
||||||
|
This repository contains a Nix flake for packaging [Bitpoll](https://github.com/fsinfuhh/Bitpoll), a web application for scheduling meetings and general polling.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Complete Nix Package**: Bitpoll packaged as a Nix derivation with all Python dependencies
|
||||||
|
- **NixOS Service Module**: Ready-to-use systemd service with PostgreSQL integration
|
||||||
|
- **Security Hardened**: Runs with minimal privileges and security restrictions
|
||||||
|
- **Configurable**: All major settings exposed as NixOS options
|
||||||
|
- **Production Ready**: Uses uWSGI with proper process management
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Add to your NixOS configuration
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
bitpoll.url = "github:your-username/bitpoll-nix";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, bitpoll }: {
|
||||||
|
nixosConfigurations.your-host = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
bitpoll.nixosModules.default
|
||||||
|
{
|
||||||
|
services.bitpoll = {
|
||||||
|
enable = true;
|
||||||
|
secretKey = "your-secret-key-here";
|
||||||
|
encryptionKey = "your-encryption-key-here";
|
||||||
|
allowedHosts = [ "your-domain.com" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate required keys
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate Django secret key
|
||||||
|
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
|
||||||
|
|
||||||
|
# Generate field encryption key (32 bytes, base64 encoded)
|
||||||
|
python -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nixos-rebuild switch --flake .#your-host
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.bitpoll = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Required security keys
|
||||||
|
secretKey = "your-django-secret-key";
|
||||||
|
encryptionKey = "your-field-encryption-key";
|
||||||
|
|
||||||
|
# Network settings
|
||||||
|
listenAddress = "127.0.0.1";
|
||||||
|
port = 3008; # uWSGI socket
|
||||||
|
httpPort = 3009; # HTTP port (null to disable)
|
||||||
|
|
||||||
|
# Django settings
|
||||||
|
debug = false;
|
||||||
|
allowedHosts = [ "your-domain.com" ];
|
||||||
|
language = "en-us";
|
||||||
|
timezone = "Europe/Berlin";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Configuration
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.bitpoll = {
|
||||||
|
# PostgreSQL is enabled by default
|
||||||
|
enablePostgreSQL = true;
|
||||||
|
|
||||||
|
database = {
|
||||||
|
name = "bitpoll";
|
||||||
|
user = "bitpoll";
|
||||||
|
password = ""; # Leave empty for peer authentication
|
||||||
|
host = "localhost";
|
||||||
|
port = 5432;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Tuning
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.bitpoll = {
|
||||||
|
# uWSGI process management
|
||||||
|
processes = 8; # Max processes
|
||||||
|
threads = 4; # Threads per process
|
||||||
|
cheaperProcesses = 2; # Min processes
|
||||||
|
|
||||||
|
# Additional uWSGI configuration
|
||||||
|
extraUwsgiConfig = ''
|
||||||
|
max-requests = 1000
|
||||||
|
reload-on-rss = 512
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.bitpoll = {
|
||||||
|
# Additional Django settings
|
||||||
|
extraSettings = {
|
||||||
|
PIPELINE_LOCAL = {
|
||||||
|
JS_COMPRESSOR = "pipeline.compressors.uglifyjs.UglifyJSCompressor";
|
||||||
|
CSS_COMPRESSOR = "pipeline.compressors.cssmin.CSSMinCompressor";
|
||||||
|
};
|
||||||
|
CSP_ADDITIONAL_SCRIPT_SRC = [ "your-analytics-domain.com" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reverse Proxy Setup
|
||||||
|
|
||||||
|
### Nginx Example
|
||||||
|
|
||||||
|
```nix
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."your-domain.com" = {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3009";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"/static/" = {
|
||||||
|
alias = "/var/lib/bitpoll/static/";
|
||||||
|
extraConfig = ''
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
All persistent data is stored in `/var/lib/bitpoll/`:
|
||||||
|
- `media/` - User uploaded files
|
||||||
|
- `static/` - Collected static files
|
||||||
|
- Database data (if using PostgreSQL, stored in PostgreSQL data directory)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The service runs with extensive security hardening:
|
||||||
|
- Dedicated user account (`bitpoll`)
|
||||||
|
- Restricted filesystem access
|
||||||
|
- No network access except required ports
|
||||||
|
- Memory execution protection
|
||||||
|
- System call filtering
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building the package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix build .#bitpoll
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development shell
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing the module
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nixos-rebuild build-vm --flake .#test-vm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check service status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status bitpoll
|
||||||
|
journalctl -u bitpoll -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PostgreSQL status
|
||||||
|
systemctl status postgresql
|
||||||
|
|
||||||
|
# Connect to database
|
||||||
|
sudo -u postgres psql bitpoll
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix data directory permissions
|
||||||
|
sudo chown -R bitpoll:bitpoll /var/lib/bitpoll
|
||||||
|
sudo chmod -R u=rwX,g=rX,o= /var/lib/bitpoll
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This packaging is released under the same license as Bitpoll (GPL-3.0).
|
160
example-configuration.nix
Normal file
160
example-configuration.nix
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
# Example NixOS configuration for Bitpoll
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
# Import the Bitpoll module
|
||||||
|
./module.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
# Enable Bitpoll service
|
||||||
|
services.bitpoll = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Required security keys (generate these!)
|
||||||
|
secretKey = "CHANGE-ME-django-secret-key-here";
|
||||||
|
encryptionKey = "CHANGE-ME-field-encryption-key-here";
|
||||||
|
|
||||||
|
# Network configuration
|
||||||
|
listenAddress = "127.0.0.1";
|
||||||
|
port = 3008; # uWSGI socket port
|
||||||
|
httpPort = 3009; # HTTP port for direct access
|
||||||
|
|
||||||
|
# Django settings
|
||||||
|
debug = false;
|
||||||
|
allowedHosts = [ "localhost" "bitpoll.example.com" ];
|
||||||
|
language = "en-us";
|
||||||
|
timezone = "Europe/Berlin";
|
||||||
|
|
||||||
|
# Database configuration (PostgreSQL is auto-configured)
|
||||||
|
database = {
|
||||||
|
name = "bitpoll";
|
||||||
|
user = "bitpoll";
|
||||||
|
password = ""; # Empty for peer authentication
|
||||||
|
host = "localhost";
|
||||||
|
port = 5432;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Performance settings
|
||||||
|
processes = 4; # Adjust based on your server
|
||||||
|
threads = 2;
|
||||||
|
cheaperProcesses = 1;
|
||||||
|
|
||||||
|
# Additional Django settings
|
||||||
|
extraSettings = {
|
||||||
|
# Pipeline configuration for asset compression
|
||||||
|
PIPELINE_LOCAL = {
|
||||||
|
JS_COMPRESSOR = "pipeline.compressors.uglifyjs.UglifyJSCompressor";
|
||||||
|
CSS_COMPRESSOR = "pipeline.compressors.cssmin.CSSMinCompressor";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
CSP_ADDITIONAL_SCRIPT_SRC = [ ];
|
||||||
|
|
||||||
|
# Additional installed apps (if needed)
|
||||||
|
INSTALLED_APPS_LOCAL = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Additional uWSGI configuration
|
||||||
|
extraUwsgiConfig = ''
|
||||||
|
# Reload workers after 1000 requests to prevent memory leaks
|
||||||
|
max-requests = 1000
|
||||||
|
|
||||||
|
# Reload if memory usage exceeds 512MB
|
||||||
|
reload-on-rss = 512
|
||||||
|
|
||||||
|
# Enable stats server (optional, for monitoring)
|
||||||
|
# stats = 127.0.0.1:9191
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Nginx reverse proxy configuration
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
virtualHosts."bitpoll.example.com" = {
|
||||||
|
# Enable HTTPS with Let's Encrypt
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
|
||||||
|
locations = {
|
||||||
|
# Proxy all requests to Bitpoll
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3009";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Increase timeouts for long-running requests
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Serve static files directly from Nginx for better performance
|
||||||
|
"/static/" = {
|
||||||
|
alias = "/var/lib/bitpoll/static/";
|
||||||
|
extraConfig = ''
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Serve media files (user uploads)
|
||||||
|
"/media/" = {
|
||||||
|
alias = "/var/lib/bitpoll/media/";
|
||||||
|
extraConfig = ''
|
||||||
|
expires 1d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# ACME configuration for Let's Encrypt
|
||||||
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults.email = "admin@example.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Firewall configuration
|
||||||
|
networking.firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowedTCPPorts = [ 80 443 ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Optional: Backup configuration
|
||||||
|
services.restic.backups.bitpoll = {
|
||||||
|
initialize = true;
|
||||||
|
repository = "/backup/bitpoll";
|
||||||
|
passwordFile = "/etc/nixos/secrets/restic-password";
|
||||||
|
paths = [ "/var/lib/bitpoll" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "daily";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Optional: Log rotation
|
||||||
|
services.logrotate = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
"/var/log/bitpoll/*.log" = {
|
||||||
|
frequency = "daily";
|
||||||
|
rotate = 30;
|
||||||
|
compress = true;
|
||||||
|
delaycompress = true;
|
||||||
|
missingok = true;
|
||||||
|
notifempty = true;
|
||||||
|
create = "644 bitpoll bitpoll";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
42
flake.nix
Normal file
42
flake.nix
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
description = "Bitpoll - A web application for scheduling meetings and general polling";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
bitpoll = pkgs.callPackage ./package.nix { };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = {
|
||||||
|
default = bitpoll;
|
||||||
|
bitpoll = bitpoll;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
python3
|
||||||
|
python3Packages.pip
|
||||||
|
python3Packages.virtualenv
|
||||||
|
postgresql
|
||||||
|
uwsgi
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
echo "Bitpoll development environment"
|
||||||
|
echo "Run 'nix build' to build the package"
|
||||||
|
echo "Run 'nixos-rebuild switch --flake .#' to deploy the service"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) // {
|
||||||
|
nixosModules = {
|
||||||
|
default = import ./module.nix;
|
||||||
|
bitpoll = import ./module.nix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
349
module.nix
Normal file
349
module.nix
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.bitpoll;
|
||||||
|
|
||||||
|
# Persistent data directory
|
||||||
|
dataDir = "/var/lib/bitpoll";
|
||||||
|
|
||||||
|
# Generate Django settings file
|
||||||
|
settingsFile = pkgs.writeText "bitpoll-settings.py" ''
|
||||||
|
# Bitpoll NixOS Configuration
|
||||||
|
import os
|
||||||
|
from bitpoll.settings.production import *
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
SECRET_KEY = '${cfg.secretKey}'
|
||||||
|
FIELD_ENCRYPTION_KEY = '${cfg.encryptionKey}'
|
||||||
|
DEBUG = ${boolToString cfg.debug}
|
||||||
|
ALLOWED_HOSTS = ${builtins.toJSON cfg.allowedHosts}
|
||||||
|
|
||||||
|
# Localization
|
||||||
|
LANGUAGE_CODE = '${cfg.language}'
|
||||||
|
TIME_ZONE = '${cfg.timezone}'
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': '${cfg.database.name}',
|
||||||
|
'USER': '${cfg.database.user}',
|
||||||
|
'PASSWORD': '${cfg.database.password}',
|
||||||
|
'HOST': '${cfg.database.host}',
|
||||||
|
'PORT': '${toString cfg.database.port}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# File storage paths
|
||||||
|
MEDIA_ROOT = '${dataDir}/media'
|
||||||
|
STATIC_ROOT = '${dataDir}/static'
|
||||||
|
|
||||||
|
# Additional settings
|
||||||
|
${concatStringsSep "\n" (mapAttrsToList (k: v: "${k} = ${builtins.toJSON v}") cfg.extraSettings)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Generate uWSGI configuration
|
||||||
|
uwsgiConfig = pkgs.writeText "uwsgi.ini" ''
|
||||||
|
[uwsgi]
|
||||||
|
procname-master = uwsgi bitpoll
|
||||||
|
master = true
|
||||||
|
socket = ${cfg.listenAddress}:${toString cfg.port}
|
||||||
|
${optionalString (cfg.httpPort != null) "http-socket = ${cfg.listenAddress}:${toString cfg.httpPort}"}
|
||||||
|
|
||||||
|
plugins = python3
|
||||||
|
|
||||||
|
chdir = ${dataDir}
|
||||||
|
virtualenv = ${cfg.package}
|
||||||
|
pythonpath = ${cfg.package}/lib/python*/site-packages
|
||||||
|
|
||||||
|
module = bitpoll.wsgi:application
|
||||||
|
env = DJANGO_SETTINGS_MODULE=bitpoll.settings
|
||||||
|
env = BITPOLL_SETTINGS_FILE=${settingsFile}
|
||||||
|
env = LANG=C.UTF-8
|
||||||
|
env = LC_ALL=C.UTF-8
|
||||||
|
|
||||||
|
# Process management
|
||||||
|
uid = ${cfg.user}
|
||||||
|
gid = ${cfg.group}
|
||||||
|
umask = 027
|
||||||
|
|
||||||
|
processes = ${toString cfg.processes}
|
||||||
|
threads = ${toString cfg.threads}
|
||||||
|
cheaper = ${toString cfg.cheaperProcesses}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
disable-logging = ${boolToString cfg.disableLogging}
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
static-map = /static=${dataDir}/static
|
||||||
|
|
||||||
|
# Additional uWSGI options
|
||||||
|
${cfg.extraUwsgiConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.services.bitpoll = {
|
||||||
|
enable = mkEnableOption "Bitpoll polling application";
|
||||||
|
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.bitpoll or (pkgs.callPackage ./package.nix { });
|
||||||
|
description = "The Bitpoll package to use";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "bitpoll";
|
||||||
|
description = "User account under which Bitpoll runs";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "bitpoll";
|
||||||
|
description = "Group under which Bitpoll runs";
|
||||||
|
};
|
||||||
|
|
||||||
|
listenAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen on";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3008;
|
||||||
|
description = "Port for uWSGI socket";
|
||||||
|
};
|
||||||
|
|
||||||
|
httpPort = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
default = 3009;
|
||||||
|
description = "Port for HTTP socket (null to disable)";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Django settings
|
||||||
|
secretKey = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Django secret key";
|
||||||
|
example = "your-secret-key-here";
|
||||||
|
};
|
||||||
|
|
||||||
|
encryptionKey = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Field encryption key";
|
||||||
|
example = "BnEAJ5eEXb4HfYbaCPuW5RKQSoO02Uhz1RH93eQz0GM=";
|
||||||
|
};
|
||||||
|
|
||||||
|
debug = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable Django debug mode";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedHosts = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ "*" ];
|
||||||
|
description = "List of allowed hosts";
|
||||||
|
};
|
||||||
|
|
||||||
|
language = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "en-us";
|
||||||
|
description = "Language code";
|
||||||
|
};
|
||||||
|
|
||||||
|
timezone = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "Europe/Berlin";
|
||||||
|
description = "Time zone";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
database = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "bitpoll";
|
||||||
|
description = "Database name";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "bitpoll";
|
||||||
|
description = "Database user";
|
||||||
|
};
|
||||||
|
|
||||||
|
password = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Database password";
|
||||||
|
};
|
||||||
|
|
||||||
|
host = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "localhost";
|
||||||
|
description = "Database host";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 5432;
|
||||||
|
description = "Database port";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# uWSGI settings
|
||||||
|
processes = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 8;
|
||||||
|
description = "Number of uWSGI processes";
|
||||||
|
};
|
||||||
|
|
||||||
|
threads = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 4;
|
||||||
|
description = "Number of threads per process";
|
||||||
|
};
|
||||||
|
|
||||||
|
cheaperProcesses = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 2;
|
||||||
|
description = "Minimum number of processes";
|
||||||
|
};
|
||||||
|
|
||||||
|
disableLogging = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Disable uWSGI request logging";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraUwsgiConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
description = "Additional uWSGI configuration";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraSettings = mkOption {
|
||||||
|
type = types.attrsOf types.anything;
|
||||||
|
default = { };
|
||||||
|
description = "Additional Django settings";
|
||||||
|
};
|
||||||
|
|
||||||
|
# PostgreSQL integration
|
||||||
|
enablePostgreSQL = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Enable and configure PostgreSQL";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
# PostgreSQL setup
|
||||||
|
services.postgresql = mkIf cfg.enablePostgreSQL {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [ cfg.database.name ];
|
||||||
|
ensureUsers = [{
|
||||||
|
name = cfg.database.user;
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create user and group
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
home = dataDir;
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.groups.${cfg.group} = { };
|
||||||
|
|
||||||
|
# Create data directories
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
"d ${dataDir}/media 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
"d ${dataDir}/static 0750 ${cfg.user} ${cfg.group} -"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Bitpoll service
|
||||||
|
systemd.services.bitpoll = {
|
||||||
|
description = "Bitpoll polling application";
|
||||||
|
after = [ "network.target" ] ++ optional cfg.enablePostgreSQL "postgresql.service";
|
||||||
|
wants = optional cfg.enablePostgreSQL "postgresql.service";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
DJANGO_SETTINGS_MODULE = "bitpoll.settings";
|
||||||
|
BITPOLL_SETTINGS_FILE = "${settingsFile}";
|
||||||
|
PYTHONPATH = "${cfg.package}/lib/python*/site-packages";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "notify";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = dataDir;
|
||||||
|
ExecStart = "${pkgs.uwsgi}/bin/uwsgi --ini ${uwsgiConfig}";
|
||||||
|
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||||
|
KillMode = "mixed";
|
||||||
|
KillSignal = "SIGINT";
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ReadWritePaths = [ dataDir ];
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
DeviceAllow = "";
|
||||||
|
LockPersonality = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
ProcSubset = "pid";
|
||||||
|
RemoveIPC = true;
|
||||||
|
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
|
||||||
|
UMask = "0027";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Pre-start script for migrations and static files
|
||||||
|
preStart = ''
|
||||||
|
# Wait for database to be ready
|
||||||
|
${optionalString cfg.enablePostgreSQL ''
|
||||||
|
while ! ${pkgs.postgresql}/bin/pg_isready -h ${cfg.database.host} -p ${toString cfg.database.port} -U ${cfg.database.user} -d ${cfg.database.name}; do
|
||||||
|
echo "Waiting for PostgreSQL..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
''}
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
cd ${dataDir}
|
||||||
|
export DJANGO_SETTINGS_MODULE=bitpoll.settings
|
||||||
|
export BITPOLL_SETTINGS_FILE=${settingsFile}
|
||||||
|
export PYTHONPATH=${cfg.package}/lib/python*/site-packages
|
||||||
|
|
||||||
|
${cfg.package}/bin/python ${cfg.package}/manage.py migrate --noinput
|
||||||
|
${cfg.package}/bin/python ${cfg.package}/manage.py collectstatic --noinput --clear
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chown -R ${cfg.user}:${cfg.group} ${dataDir}
|
||||||
|
chmod -R u=rwX,g=rX,o= ${dataDir}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Open firewall ports if needed
|
||||||
|
networking.firewall.allowedTCPPorts = mkIf (cfg.httpPort != null) [ cfg.httpPort ];
|
||||||
|
};
|
||||||
|
}
|
116
package.nix
Normal file
116
package.nix
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{ lib
|
||||||
|
, buildPythonApplication
|
||||||
|
, fetchFromGitHub
|
||||||
|
, python3Packages
|
||||||
|
, gettext
|
||||||
|
, libsass
|
||||||
|
, pkg-config
|
||||||
|
, postgresql
|
||||||
|
, uwsgi
|
||||||
|
}:
|
||||||
|
|
||||||
|
buildPythonApplication rec {
|
||||||
|
pname = "bitpoll";
|
||||||
|
version = "unstable-2024-11-23";
|
||||||
|
format = "setuptools";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "fsinfuhh";
|
||||||
|
repo = "Bitpoll";
|
||||||
|
rev = "4a3e6a5e3500308a428a6c7644f50d423adca6fc";
|
||||||
|
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
gettext
|
||||||
|
pkg-config
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
libsass
|
||||||
|
postgresql
|
||||||
|
];
|
||||||
|
|
||||||
|
propagatedBuildInputs = with python3Packages; [
|
||||||
|
# Core Django dependencies
|
||||||
|
django
|
||||||
|
django-auth-ldap
|
||||||
|
django-encrypted-model-fields
|
||||||
|
django-friendly-tag-loader
|
||||||
|
django-markdownify
|
||||||
|
django-pipeline
|
||||||
|
django-token-bucket
|
||||||
|
django-widget-tweaks
|
||||||
|
|
||||||
|
# Database
|
||||||
|
psycopg2
|
||||||
|
|
||||||
|
# Calendar and date handling
|
||||||
|
caldav
|
||||||
|
icalendar
|
||||||
|
python-dateutil
|
||||||
|
pytz
|
||||||
|
recurring-ical-events
|
||||||
|
x-wr-timezone
|
||||||
|
|
||||||
|
# Authentication and security
|
||||||
|
simple-openid-connect
|
||||||
|
cryptography
|
||||||
|
cryptojwt
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
bleach
|
||||||
|
furl
|
||||||
|
lxml
|
||||||
|
markdown
|
||||||
|
requests
|
||||||
|
sentry-sdk
|
||||||
|
|
||||||
|
# SASS compilation
|
||||||
|
libsasscompiler
|
||||||
|
|
||||||
|
# Other dependencies
|
||||||
|
pydantic
|
||||||
|
six
|
||||||
|
vobject
|
||||||
|
];
|
||||||
|
|
||||||
|
# Create a setup.py since the project doesn't have one
|
||||||
|
preBuild = ''
|
||||||
|
cat > setup.py << EOF
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='bitpoll',
|
||||||
|
version='${version}',
|
||||||
|
packages=find_packages(),
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=[],
|
||||||
|
scripts=['manage.py'],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'bitpoll-manage=manage:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Compile messages and collect static files
|
||||||
|
postBuild = ''
|
||||||
|
export DJANGO_SETTINGS_MODULE=bitpoll.settings.production
|
||||||
|
python manage.py compilemessages
|
||||||
|
python manage.py collectstatic --noinput --clear
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Skip tests for now as they require additional setup
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "A web application for scheduling meetings and general polling";
|
||||||
|
homepage = "https://github.com/fsinfuhh/Bitpoll";
|
||||||
|
license = licenses.gpl3Only;
|
||||||
|
maintainers = [ ];
|
||||||
|
platforms = platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue