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