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