{ description = "Bitpoll - A web application for scheduling meetings and general polling"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; # Pin to current master commit bitpollSrc = pkgs.fetchFromGitHub { owner = "fsinfuhh"; repo = "Bitpoll"; rev = "4a3e6a5e3500308a428a6c7644f50d423adca6fc"; sha256 = "sha256-R4OwQdSJu9+EAlkhYOEe2ZOrS9oOA1ifg/iY6uzYSpE="; }; # Create django-simple-csp derivation django-simple-csp = pkgs.python3.pkgs.buildPythonPackage rec { pname = "django-simple-csp"; version = "0.4.dev1"; src = pkgs.fetchFromGitHub { owner = "fsinfuhh"; repo = "django-simple-csp"; rev = "207afedec1bf28af5de070da919061474b1f6a21"; sha256 = "sha256-1rvdplHPjVZ6li831/sQT+lL4rE8ME1eLfeSd0EKHb4="; }; propagatedBuildInputs = with pkgs.python3.pkgs; [ django ]; doCheck = false; # Skip tests during build meta = with pkgs.lib; { description = "Django Content Security Policy support"; homepage = "https://github.com/fsinfuhh/django-simple-csp"; license = licenses.mit; }; }; # Create settings_local.py for production settingsLocal = pkgs.writeText "settings_local.py" '' import os import secrets # Generate secret key if not provided via environment SECRET_KEY = os.environ.get('BITPOLL_SECRET_KEY', secrets.token_urlsafe(50)) # Generate field encryption key if not provided via environment FIELD_ENCRYPTION_KEY = os.environ.get('BITPOLL_FIELD_ENCRYPTION_KEY', secrets.token_urlsafe(32) + "=") DEBUG = False ALLOWED_HOSTS = ['*'] # Configure appropriately for production # SQLite database configuration DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': '/var/lib/bitpoll/db.sqlite3', } } # Static files STATIC_ROOT = '/var/lib/bitpoll/static' # Media files MEDIA_ROOT = '/var/lib/bitpoll/media' # Locale LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' # Site configuration SITE_NAME = 'Bitpoll' # Additional apps INSTALLED_APPS_LOCAL = [] MIDDLEWARE_LOCAL = [] PIPELINE_LOCAL = {} # Registration enabled by default REGISTER_ENABLED = True GROUP_MANAGEMENT = True ''; # Create Python environment with all dependencies pythonEnv = pkgs.python3.withPackages (ps: with ps; [ # Core Django dependencies django django-simple-csp # Calendar and date handling caldav icalendar python-dateutil pytz vobject recurring-ical-events # Crypto and security cryptography django-encrypted-model-fields # Web and HTTP requests # Markup and styling markdown bleach # Data handling pydantic # Database psycopg2 # Utilities six lxml # Additional dependencies setuptools wheel pip # Available Django packages # Note: Some packages will need to be installed via pip in postInstall ]); bitpoll = pkgs.stdenv.mkDerivation rec { pname = "bitpoll"; version = "master-${builtins.substring 0 7 bitpollSrc.rev}"; src = bitpollSrc; nativeBuildInputs = with pkgs; [ gettext pythonEnv ]; buildInputs = with pkgs; [ # LDAP support openldap cyrus_sasl ]; # Don't run tests during build doCheck = false; preBuild = '' # Copy our settings file cp ${settingsLocal} bitpoll/settings_local.py ''; installPhase = '' runHook preInstall mkdir -p $out/share/bitpoll cp -r . $out/share/bitpoll/ # Install missing Python packages via pip export PYTHONPATH=$out/lib/python3.12/site-packages:$PYTHONPATH mkdir -p $out/lib/python3.12/site-packages ${pythonEnv}/bin/pip install --target $out/lib/python3.12/site-packages \ django-markdownify==0.9.5 \ django-pipeline==3.1.0 \ django-friendly-tag-loader==1.3.1 \ django-token-bucket==0.2.4 \ django-widget-tweaks==1.5.0 \ libsasscompiler==0.1.9 # Create wrapper script mkdir -p $out/bin cat > $out/bin/bitpoll-manage << EOF #!/bin/sh cd $out/share/bitpoll export PYTHONPATH=$out/lib/python3.12/site-packages:$out/share/bitpoll:\$PYTHONPATH exec ${pythonEnv}/bin/python manage.py "\$@" EOF chmod +x $out/bin/bitpoll-manage # Create server script cat > $out/bin/bitpoll-server << EOF #!/bin/sh cd $out/share/bitpoll export PYTHONPATH=$out/lib/python3.12/site-packages:$out/share/bitpoll:\$PYTHONPATH exec ${pythonEnv}/bin/python manage.py runserver "\$@" EOF chmod +x $out/bin/bitpoll-server runHook postInstall ''; meta = with pkgs.lib; { description = "A web application for scheduling meetings and general polling"; homepage = "https://github.com/fsinfuhh/Bitpoll"; license = licenses.gpl3Only; maintainers = [ ]; platforms = platforms.unix; }; }; in { packages = { default = bitpoll; bitpoll = bitpoll; }; apps = { default = { type = "app"; program = "${bitpoll}/bin/bitpoll-server"; meta = { description = "Run Bitpoll development server"; }; }; bitpoll-manage = { type = "app"; program = "${bitpoll}/bin/bitpoll-manage"; meta = { description = "Run Bitpoll management commands"; }; }; }; devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ python3 python3Packages.pip python3Packages.virtualenv gettext openldap cyrus_sasl ]; }; } ) // { nixosModules.default = self.nixosModules.bitpoll; nixosModules.bitpoll = { config, lib, pkgs, ... }: with lib; let cfg = config.services.bitpoll; bitpollPackage = self.packages.${pkgs.system}.bitpoll; in { options.services.bitpoll = { enable = mkEnableOption "Bitpoll service"; package = mkOption { type = types.package; default = bitpollPackage; description = "The Bitpoll package to use"; }; port = mkOption { type = types.port; default = 8000; description = "Port to listen on"; }; host = mkOption { type = types.str; default = "127.0.0.1"; description = "Host to bind to"; }; dataDir = mkOption { type = types.path; default = "/var/lib/bitpoll"; description = "Directory to store Bitpoll data"; }; secretKeyFile = mkOption { type = types.nullOr types.path; default = null; description = "File containing the Django secret key"; }; allowedHosts = mkOption { type = types.listOf types.str; default = [ "localhost" "127.0.0.1" ]; description = "List of allowed hosts"; }; extraSettings = mkOption { type = types.lines; default = ""; description = "Extra settings to append to settings_local.py"; }; }; config = mkIf cfg.enable { users.users.bitpoll = { isSystemUser = true; group = "bitpoll"; home = cfg.dataDir; createHome = true; }; users.groups.bitpoll = {}; systemd.services.bitpoll = { description = "Bitpoll web application"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment = { PYTHONPATH = "${cfg.package}/share/bitpoll"; BITPOLL_DATA_DIR = cfg.dataDir; } // optionalAttrs (cfg.secretKeyFile != null) { BITPOLL_SECRET_KEY_FILE = cfg.secretKeyFile; }; preStart = '' # Ensure data directory exists and has correct permissions mkdir -p ${cfg.dataDir}/{static,media} chown -R bitpoll:bitpoll ${cfg.dataDir} chmod 750 ${cfg.dataDir} # Create runtime settings if secret key file is provided if [ -n "''${BITPOLL_SECRET_KEY_FILE:-}" ] && [ -f "$BITPOLL_SECRET_KEY_FILE" ]; then export BITPOLL_SECRET_KEY="$(cat "$BITPOLL_SECRET_KEY_FILE")" fi # Run database migrations cd ${cfg.package}/share/bitpoll ${cfg.package}/bin/bitpoll-manage migrate --noinput # Collect static files ${cfg.package}/bin/bitpoll-manage collectstatic --noinput --clear # Compile messages ${cfg.package}/bin/bitpoll-manage compilemessages ''; serviceConfig = { Type = "exec"; User = "bitpoll"; Group = "bitpoll"; WorkingDirectory = "${cfg.package}/share/bitpoll"; ExecStart = "${cfg.package}/bin/bitpoll-server ${cfg.host}:${toString cfg.port}"; Restart = "always"; RestartSec = "10s"; # Security settings NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ cfg.dataDir ]; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; }; }; # Open firewall port if needed # networking.firewall.allowedTCPPorts = [ cfg.port ]; }; }; }; }