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