From 67ce1357fac761342cdb72c0b5e14fb0bb4ca24c Mon Sep 17 00:00:00 2001 From: Thomas Strobel <4ZKTUB6TEP74PYJOPWIR013S2AV29YUBW5F9ZH2F4D5UMJUJ6S@hash.domains> Date: Wed, 22 Nov 2017 12:12:27 +0100 Subject: [PATCH] nixos: init module: restya-board --- nixos/modules/module-list.nix | 1 + .../services/web-apps/restya-board.nix | 384 ++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 nixos/modules/services/web-apps/restya-board.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index d6360648964..82e06596820 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -608,6 +608,7 @@ ./services/web-apps/pgpkeyserver-lite.nix ./services/web-apps/piwik.nix ./services/web-apps/pump.io.nix + ./services/web-apps/restya-board.nix ./services/web-apps/tt-rss.nix ./services/web-apps/selfoss.nix ./services/web-apps/quassel-webserver.nix diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix new file mode 100644 index 00000000000..cee725e8fe5 --- /dev/null +++ b/nixos/modules/services/web-apps/restya-board.nix @@ -0,0 +1,384 @@ +{ 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; + + runDir = "/run/restya-board"; + + poolName = "restya-board"; + phpfpmSocketName = "/var/run/phpfpm/${poolName}.sock"; + +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.str; + 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.poolConfigs = { + "${poolName}" = '' + listen = "${phpfpmSocketName}"; + listen.owner = nginx + listen.group = nginx + listen.mode = 0600 + user = ${cfg.user} + group = ${cfg.group} + 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.phpfpm.phpOptions = '' + date.timezone = "CET" + + ${optionalString (!isNull cfg.email.server) '' + SMTP = ${cfg.email.server} + smtp_port = ${toString cfg.email.port} + auth_username = ${cfg.email.login} + auth_password = ${cfg.email.password} + ''} + ''; + + 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_disable "msie6"; + + 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:${phpfpmSocketName}; + 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 (isNull cfg.database.host) 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', '$(<${cfg.database.dbPassFile})');/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 (isNull cfg.database.host) '' + 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.extraUsers.restya-board = { + isSystemUser = true; + createHome = false; + home = runDir; + group = "restya-board"; + }; + users.extraGroups.restya-board = {}; + + services.postgresql.enable = mkIf (isNull cfg.database.host) true; + + services.postgresql.identMap = optionalString (isNull cfg.database.host) + '' + restya-board-users restya-board restya_board + ''; + + services.postgresql.authentication = optionalString (isNull cfg.database.host) + '' + local restya_board all ident map=restya-board-users + ''; + + }; + +} +