at 21.11-pre 12 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5# TODO: are these php-packages needed? 6#imagick 7#php-geoip -> php.ini: extension = geoip.so 8#expat 9 10let 11 cfg = config.services.restya-board; 12 fpm = config.services.phpfpm.pools.${poolName}; 13 14 runDir = "/run/restya-board"; 15 16 poolName = "restya-board"; 17 18in 19 20{ 21 22 ###### interface 23 24 options = { 25 26 services.restya-board = { 27 28 enable = mkEnableOption "restya-board"; 29 30 dataDir = mkOption { 31 type = types.path; 32 default = "/var/lib/restya-board"; 33 example = "/var/lib/restya-board"; 34 description = '' 35 Data of the application. 36 ''; 37 }; 38 39 user = mkOption { 40 type = types.str; 41 default = "restya-board"; 42 example = "restya-board"; 43 description = '' 44 User account under which the web-application runs. 45 ''; 46 }; 47 48 group = mkOption { 49 type = types.str; 50 default = "nginx"; 51 example = "nginx"; 52 description = '' 53 Group account under which the web-application runs. 54 ''; 55 }; 56 57 virtualHost = { 58 serverName = mkOption { 59 type = types.str; 60 default = "restya.board"; 61 description = '' 62 Name of the nginx virtualhost to use. 63 ''; 64 }; 65 66 listenHost = mkOption { 67 type = types.str; 68 default = "localhost"; 69 description = '' 70 Listen address for the virtualhost to use. 71 ''; 72 }; 73 74 listenPort = mkOption { 75 type = types.int; 76 default = 3000; 77 description = '' 78 Listen port for the virtualhost to use. 79 ''; 80 }; 81 }; 82 83 database = { 84 host = mkOption { 85 type = types.nullOr types.str; 86 default = null; 87 description = '' 88 Host of the database. Leave 'null' to use a local PostgreSQL database. 89 A local PostgreSQL database is initialized automatically. 90 ''; 91 }; 92 93 port = mkOption { 94 type = types.nullOr types.int; 95 default = 5432; 96 description = '' 97 The database's port. 98 ''; 99 }; 100 101 name = mkOption { 102 type = types.str; 103 default = "restya_board"; 104 description = '' 105 Name of the database. The database must exist. 106 ''; 107 }; 108 109 user = mkOption { 110 type = types.str; 111 default = "restya_board"; 112 description = '' 113 The database user. The user must exist and have access to 114 the specified database. 115 ''; 116 }; 117 118 passwordFile = mkOption { 119 type = types.nullOr types.path; 120 default = null; 121 description = '' 122 The database user's password. 'null' if no password is set. 123 ''; 124 }; 125 }; 126 127 email = { 128 server = mkOption { 129 type = types.nullOr types.str; 130 default = null; 131 example = "localhost"; 132 description = '' 133 Hostname to send outgoing mail. Null to use the system MTA. 134 ''; 135 }; 136 137 port = mkOption { 138 type = types.int; 139 default = 25; 140 description = '' 141 Port used to connect to SMTP server. 142 ''; 143 }; 144 145 login = mkOption { 146 type = types.str; 147 default = ""; 148 description = '' 149 SMTP authentication login used when sending outgoing mail. 150 ''; 151 }; 152 153 password = mkOption { 154 type = types.str; 155 default = ""; 156 description = '' 157 SMTP authentication password used when sending outgoing mail. 158 159 ATTENTION: The password is stored world-readable in the nix-store! 160 ''; 161 }; 162 }; 163 164 timezone = mkOption { 165 type = types.lines; 166 default = "GMT"; 167 description = '' 168 Timezone the web-app runs in. 169 ''; 170 }; 171 172 }; 173 174 }; 175 176 177 ###### implementation 178 179 config = mkIf cfg.enable { 180 181 services.phpfpm.pools = { 182 ${poolName} = { 183 inherit (cfg) user group; 184 185 phpOptions = '' 186 date.timezone = "CET" 187 188 ${optionalString (cfg.email.server != null) '' 189 SMTP = ${cfg.email.server} 190 smtp_port = ${toString cfg.email.port} 191 auth_username = ${cfg.email.login} 192 auth_password = ${cfg.email.password} 193 ''} 194 ''; 195 settings = mapAttrs (name: mkDefault) { 196 "listen.owner" = "nginx"; 197 "listen.group" = "nginx"; 198 "listen.mode" = "0600"; 199 "pm" = "dynamic"; 200 "pm.max_children" = 75; 201 "pm.start_servers" = 10; 202 "pm.min_spare_servers" = 5; 203 "pm.max_spare_servers" = 20; 204 "pm.max_requests" = 500; 205 "catch_workers_output" = 1; 206 }; 207 }; 208 }; 209 210 services.nginx.enable = true; 211 services.nginx.virtualHosts.${cfg.virtualHost.serverName} = { 212 listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ]; 213 serverName = cfg.virtualHost.serverName; 214 root = runDir; 215 extraConfig = '' 216 index index.html index.php; 217 218 gzip on; 219 220 gzip_comp_level 6; 221 gzip_min_length 1100; 222 gzip_buffers 16 8k; 223 gzip_proxied any; 224 gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss; 225 226 client_max_body_size 300M; 227 228 rewrite ^/oauth/authorize$ /server/php/authorize.php last; 229 rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last; 230 rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last; 231 rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last; 232 rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last; 233 rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last; 234 ''; 235 236 locations."/".root = "${runDir}/client"; 237 238 locations."~ \\.php$" = { 239 tryFiles = "$uri =404"; 240 extraConfig = '' 241 include ${pkgs.nginx}/conf/fastcgi_params; 242 fastcgi_pass unix:${fpm.socket}; 243 fastcgi_index index.php; 244 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 245 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"; 246 ''; 247 }; 248 249 locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = { 250 root = "${runDir}/client"; 251 extraConfig = '' 252 if (-f $request_filename) { 253 break; 254 } 255 rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last; 256 add_header Cache-Control public; 257 add_header Cache-Control must-revalidate; 258 expires 7d; 259 ''; 260 }; 261 }; 262 263 systemd.services.restya-board-init = { 264 description = "Restya board initialization"; 265 serviceConfig.Type = "oneshot"; 266 serviceConfig.RemainAfterExit = true; 267 268 wantedBy = [ "multi-user.target" ]; 269 requires = [ "postgresql.service" ]; 270 after = [ "network.target" "postgresql.service" ]; 271 272 script = '' 273 rm -rf "${runDir}" 274 mkdir -m 750 -p "${runDir}" 275 cp -r "${pkgs.restya-board}/"* "${runDir}" 276 sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql" 277 rm -rf "${runDir}/media" 278 rm -rf "${runDir}/client/img" 279 chmod -R 0750 "${runDir}" 280 281 sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh 282 283 ${if (cfg.database.host == null) then '' 284 sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php" 285 sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php" 286 '' else '' 287 sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php" 288 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 289 ''} 290 sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php" 291 sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php" 292 sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php" 293 294 chmod 0400 "${runDir}/server/php/config.inc.php" 295 296 ln -sf "${cfg.dataDir}/media" "${runDir}/media" 297 ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img" 298 299 chmod g+w "${runDir}/tmp/cache" 300 chown -R "${cfg.user}"."${cfg.group}" "${runDir}" 301 302 303 mkdir -m 0750 -p "${cfg.dataDir}" 304 mkdir -m 0750 -p "${cfg.dataDir}/media" 305 mkdir -m 0750 -p "${cfg.dataDir}/client/img" 306 cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media" 307 cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img" 308 chown "${cfg.user}"."${cfg.group}" "${cfg.dataDir}" 309 chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/media" 310 chown -R "${cfg.user}"."${cfg.group}" "${cfg.dataDir}/client/img" 311 312 ${optionalString (cfg.database.host == null) '' 313 if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then 314 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ 315 ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ 316 -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'" 317 318 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ 319 ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ 320 -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0" 321 322 ${pkgs.sudo}/bin/sudo -u ${cfg.user} \ 323 ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \ 324 -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql" 325 326 touch "${cfg.dataDir}/.db-initialized" 327 fi 328 ''} 329 ''; 330 }; 331 332 systemd.timers.restya-board = { 333 description = "restya-board scripts for e.g. email notification"; 334 wantedBy = [ "timers.target" ]; 335 after = [ "restya-board-init.service" ]; 336 requires = [ "restya-board-init.service" ]; 337 timerConfig = { 338 OnUnitInactiveSec = "60s"; 339 Unit = "restya-board-timers.service"; 340 }; 341 }; 342 343 systemd.services.restya-board-timers = { 344 description = "restya-board scripts for e.g. email notification"; 345 serviceConfig.Type = "oneshot"; 346 serviceConfig.User = cfg.user; 347 348 after = [ "restya-board-init.service" ]; 349 requires = [ "restya-board-init.service" ]; 350 351 script = '' 352 /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true 353 /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true 354 /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true 355 /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true 356 /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true 357 ''; 358 }; 359 360 users.users.restya-board = { 361 isSystemUser = true; 362 createHome = false; 363 home = runDir; 364 group = "restya-board"; 365 }; 366 users.groups.restya-board = {}; 367 368 services.postgresql.enable = mkIf (cfg.database.host == null) true; 369 370 services.postgresql.identMap = optionalString (cfg.database.host == null) 371 '' 372 restya-board-users restya-board restya_board 373 ''; 374 375 services.postgresql.authentication = optionalString (cfg.database.host == null) 376 '' 377 local restya_board all ident map=restya-board-users 378 ''; 379 380 }; 381 382} 383