384 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
{ config, lib, pkgs, ... }:
 | 
						|
 | 
						|
with lib;
 | 
						|
 | 
						|
# TODO: are these php-packages needed?
 | 
						|
#imagick
 | 
						|
#php-geoip -> php.ini: extension = geoip.so
 | 
						|
#expat
 | 
						|
 | 
						|
let
 | 
						|
  cfg = config.services.restya-board;
 | 
						|
  fpm = config.services.phpfpm.pools.${poolName};
 | 
						|
 | 
						|
  runDir = "/run/restya-board";
 | 
						|
 | 
						|
  poolName = "restya-board";
 | 
						|
 | 
						|
in
 | 
						|
 | 
						|
{
 | 
						|
 | 
						|
  ###### interface
 | 
						|
 | 
						|
  options = {
 | 
						|
 | 
						|
    services.restya-board = {
 | 
						|
 | 
						|
      enable = mkEnableOption "restya-board";
 | 
						|
 | 
						|
      dataDir = mkOption {
 | 
						|
        type = types.path;
 | 
						|
        default = "/var/lib/restya-board";
 | 
						|
        example = "/var/lib/restya-board";
 | 
						|
        description = ''
 | 
						|
          Data of the application.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      user = mkOption {
 | 
						|
        type = types.str;
 | 
						|
        default = "restya-board";
 | 
						|
        example = "restya-board";
 | 
						|
        description = ''
 | 
						|
          User account under which the web-application runs.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      group = mkOption {
 | 
						|
        type = types.str;
 | 
						|
        default = "nginx";
 | 
						|
        example = "nginx";
 | 
						|
        description = ''
 | 
						|
          Group account under which the web-application runs.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      virtualHost = {
 | 
						|
        serverName = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "restya.board";
 | 
						|
          description = ''
 | 
						|
            Name of the nginx virtualhost to use.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        listenHost = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "localhost";
 | 
						|
          description = ''
 | 
						|
            Listen address for the virtualhost to use.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        listenPort = mkOption {
 | 
						|
          type = types.int;
 | 
						|
          default = 3000;
 | 
						|
          description = ''
 | 
						|
            Listen port for the virtualhost to use.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      database = {
 | 
						|
        host = mkOption {
 | 
						|
          type = types.nullOr types.str;
 | 
						|
          default = null;
 | 
						|
          description = ''
 | 
						|
            Host of the database. Leave 'null' to use a local PostgreSQL database.
 | 
						|
            A local PostgreSQL database is initialized automatically.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        port = mkOption {
 | 
						|
          type = types.nullOr types.int;
 | 
						|
          default = 5432;
 | 
						|
          description = ''
 | 
						|
            The database's port.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        name = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "restya_board";
 | 
						|
          description = ''
 | 
						|
            Name of the database. The database must exist.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        user = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "restya_board";
 | 
						|
          description = ''
 | 
						|
            The database user. The user must exist and have access to
 | 
						|
            the specified database.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        passwordFile = mkOption {
 | 
						|
          type = types.nullOr types.path;
 | 
						|
          default = null;
 | 
						|
          description = ''
 | 
						|
            The database user's password. 'null' if no password is set.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      email = {
 | 
						|
        server = mkOption {
 | 
						|
          type = types.nullOr types.str;
 | 
						|
          default = null;
 | 
						|
          example = "localhost";
 | 
						|
          description = ''
 | 
						|
            Hostname to send outgoing mail. Null to use the system MTA.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        port = mkOption {
 | 
						|
          type = types.int;
 | 
						|
          default = 25;
 | 
						|
          description = ''
 | 
						|
            Port used to connect to SMTP server.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        login = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "";
 | 
						|
          description = ''
 | 
						|
            SMTP authentication login used when sending outgoing mail.
 | 
						|
          '';
 | 
						|
        };
 | 
						|
 | 
						|
        password = mkOption {
 | 
						|
          type = types.str;
 | 
						|
          default = "";
 | 
						|
          description = ''
 | 
						|
            SMTP authentication password used when sending outgoing mail.
 | 
						|
 | 
						|
            ATTENTION: The password is stored world-readable in the nix-store!
 | 
						|
          '';
 | 
						|
        };
 | 
						|
      };
 | 
						|
 | 
						|
      timezone = mkOption {
 | 
						|
        type = types.lines;
 | 
						|
        default = "GMT";
 | 
						|
        description = ''
 | 
						|
          Timezone the web-app runs in.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
    };
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
 | 
						|
  ###### implementation
 | 
						|
 | 
						|
  config = mkIf cfg.enable {
 | 
						|
 | 
						|
    services.phpfpm.pools = {
 | 
						|
      ${poolName} = {
 | 
						|
        inherit (cfg) user group;
 | 
						|
 | 
						|
        phpOptions = ''
 | 
						|
          date.timezone = "CET"
 | 
						|
 | 
						|
          ${optionalString (cfg.email.server != null) ''
 | 
						|
            SMTP = ${cfg.email.server}
 | 
						|
            smtp_port = ${toString cfg.email.port}
 | 
						|
            auth_username = ${cfg.email.login}
 | 
						|
            auth_password = ${cfg.email.password}
 | 
						|
          ''}
 | 
						|
        '';
 | 
						|
        settings = mapAttrs (name: mkDefault) {
 | 
						|
          "listen.owner" = "nginx";
 | 
						|
          "listen.group" = "nginx";
 | 
						|
          "listen.mode" = "0600";
 | 
						|
          "pm" = "dynamic";
 | 
						|
          "pm.max_children" = 75;
 | 
						|
          "pm.start_servers" = 10;
 | 
						|
          "pm.min_spare_servers" = 5;
 | 
						|
          "pm.max_spare_servers" = 20;
 | 
						|
          "pm.max_requests" = 500;
 | 
						|
          "catch_workers_output" = 1;
 | 
						|
        };
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    services.nginx.enable = true;
 | 
						|
    services.nginx.virtualHosts.${cfg.virtualHost.serverName} = {
 | 
						|
      listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ];
 | 
						|
      serverName = cfg.virtualHost.serverName;
 | 
						|
      root = runDir;
 | 
						|
      extraConfig = ''
 | 
						|
        index index.html index.php;
 | 
						|
 | 
						|
        gzip on;
 | 
						|
 | 
						|
        gzip_comp_level 6;
 | 
						|
        gzip_min_length  1100;
 | 
						|
        gzip_buffers 16 8k;
 | 
						|
        gzip_proxied any;
 | 
						|
        gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss;
 | 
						|
 | 
						|
        client_max_body_size 300M;
 | 
						|
 | 
						|
        rewrite ^/oauth/authorize$ /server/php/authorize.php last;
 | 
						|
        rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last;
 | 
						|
        rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last;
 | 
						|
        rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last;
 | 
						|
        rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last;
 | 
						|
        rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last;
 | 
						|
      '';
 | 
						|
 | 
						|
      locations."/".root = "${runDir}/client";
 | 
						|
 | 
						|
      locations."~ \\.php$" = {
 | 
						|
        tryFiles = "$uri =404";
 | 
						|
        extraConfig = ''
 | 
						|
          include ${pkgs.nginx}/conf/fastcgi_params;
 | 
						|
          fastcgi_pass    unix:${fpm.socket};
 | 
						|
          fastcgi_index   index.php;
 | 
						|
          fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
 | 
						|
          fastcgi_param   PHP_VALUE "upload_max_filesize=9G \n post_max_size=9G \n max_execution_time=200 \n max_input_time=200 \n memory_limit=256M";
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = {
 | 
						|
        root = "${runDir}/client";
 | 
						|
        extraConfig = ''
 | 
						|
          if (-f $request_filename) {
 | 
						|
                  break;
 | 
						|
          }
 | 
						|
          rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last;
 | 
						|
          add_header        Cache-Control public;
 | 
						|
          add_header        Cache-Control must-revalidate;
 | 
						|
          expires           7d;
 | 
						|
        '';
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    systemd.services.restya-board-init = {
 | 
						|
      description = "Restya board initialization";
 | 
						|
      serviceConfig.Type = "oneshot";
 | 
						|
      serviceConfig.RemainAfterExit = true;
 | 
						|
 | 
						|
      wantedBy = [ "multi-user.target" ];
 | 
						|
      requires = [ "postgresql.service" ];
 | 
						|
      after = [ "network.target" "postgresql.service" ];
 | 
						|
 | 
						|
      script = ''
 | 
						|
        rm -rf "${runDir}"
 | 
						|
        mkdir -m 750 -p "${runDir}"
 | 
						|
        cp -r "${pkgs.restya-board}/"* "${runDir}"
 | 
						|
        sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql"
 | 
						|
        rm -rf "${runDir}/media"
 | 
						|
        rm -rf "${runDir}/client/img"
 | 
						|
        chmod -R 0750 "${runDir}"
 | 
						|
 | 
						|
        sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh
 | 
						|
 | 
						|
        ${if (cfg.database.host == null) then ''
 | 
						|
          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
        '' else ''
 | 
						|
          sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
          sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'file_get_contents(${cfg.database.passwordFile})'"});/g" "${runDir}/server/php/config.inc.php
 | 
						|
        ''}
 | 
						|
        sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
        sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
        sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php"
 | 
						|
 | 
						|
        chmod 0400 "${runDir}/server/php/config.inc.php"
 | 
						|
 | 
						|
        ln -sf "${cfg.dataDir}/media" "${runDir}/media"
 | 
						|
        ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
 | 
						|
 | 
						|
        chmod g+w "${runDir}/tmp/cache"
 | 
						|
        chown -R "${cfg.user}"."${cfg.group}" "${runDir}"
 | 
						|
 | 
						|
 | 
						|
        mkdir -m 0750 -p "${cfg.dataDir}"
 | 
						|
        mkdir -m 0750 -p "${cfg.dataDir}/media"
 | 
						|
        mkdir -m 0750 -p "${cfg.dataDir}/client/img"
 | 
						|
        cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media"
 | 
						|
        cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img"
 | 
						|
        chown "${cfg.user}"."${cfg.group}" "${cfg.dataDir}"
 | 
						|
        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/media"
 | 
						|
        chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/client/img"
 | 
						|
 | 
						|
        ${optionalString (cfg.database.host == null) ''
 | 
						|
          if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then
 | 
						|
            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
 | 
						|
              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
 | 
						|
              -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'"
 | 
						|
 | 
						|
            ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
 | 
						|
              ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
 | 
						|
              -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0"
 | 
						|
 | 
						|
            ${pkgs.sudo}/bin/sudo -u ${cfg.user} \
 | 
						|
              ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \
 | 
						|
              -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql"
 | 
						|
 | 
						|
            touch "${cfg.dataDir}/.db-initialized"
 | 
						|
          fi
 | 
						|
        ''}
 | 
						|
      '';
 | 
						|
    };
 | 
						|
 | 
						|
    systemd.timers.restya-board = {
 | 
						|
      description = "restya-board scripts for e.g. email notification";
 | 
						|
      wantedBy = [ "timers.target" ];
 | 
						|
      after = [ "restya-board-init.service" ];
 | 
						|
      requires = [ "restya-board-init.service" ];
 | 
						|
      timerConfig = {
 | 
						|
        OnUnitInactiveSec = "60s";
 | 
						|
        Unit = "restya-board-timers.service";
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    systemd.services.restya-board-timers = {
 | 
						|
      description = "restya-board scripts for e.g. email notification";
 | 
						|
      serviceConfig.Type = "oneshot";
 | 
						|
      serviceConfig.User = cfg.user;
 | 
						|
 | 
						|
      after = [ "restya-board-init.service" ];
 | 
						|
      requires = [ "restya-board-init.service" ];
 | 
						|
 | 
						|
      script = ''
 | 
						|
        /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true
 | 
						|
        /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true
 | 
						|
        /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true
 | 
						|
        /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true
 | 
						|
        /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true
 | 
						|
      '';
 | 
						|
    };
 | 
						|
 | 
						|
    users.users.restya-board = {
 | 
						|
      isSystemUser = true;
 | 
						|
      createHome = false;
 | 
						|
      home = runDir;
 | 
						|
      group  = "restya-board";
 | 
						|
    };
 | 
						|
    users.groups.restya-board = {};
 | 
						|
 | 
						|
    services.postgresql.enable = mkIf (cfg.database.host == null) true;
 | 
						|
 | 
						|
    services.postgresql.identMap = optionalString (cfg.database.host == null)
 | 
						|
      ''
 | 
						|
        restya-board-users restya-board restya_board
 | 
						|
      '';
 | 
						|
 | 
						|
    services.postgresql.authentication = optionalString (cfg.database.host == null)
 | 
						|
      ''
 | 
						|
        local restya_board all ident map=restya-board-users
 | 
						|
      '';
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
}
 | 
						|
 |