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