···
1
+
{ config, lib, pkgs, ... }:
4
+
inherit (builtins) toString;
5
+
inherit (lib) types mkIf mkOption mkDefault;
6
+
inherit (lib) optional optionals optionalAttrs optionalString;
8
+
inherit (pkgs) sqlite;
10
+
format = pkgs.formats.ini {
11
+
mkKeyValue = key: value:
13
+
value' = if builtins.isNull value then
15
+
else if builtins.isBool value then
16
+
if value == true then "true" else "false"
19
+
in "${key} = ${value'}";
22
+
cfg = config.services.writefreely;
24
+
isSqlite = cfg.database.type == "sqlite3";
25
+
isMysql = cfg.database.type == "mysql";
26
+
isMysqlLocal = isMysql && cfg.database.createLocally == true;
28
+
hostProtocol = if cfg.acme.enable then "https" else "http";
30
+
settings = cfg.settings // {
31
+
app = cfg.settings.app or { } // {
32
+
host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
35
+
database = if cfg.database.type == "sqlite3" then {
37
+
filename = cfg.settings.database.filename or "writefreely.db";
38
+
database = cfg.database.name;
41
+
username = cfg.database.user;
42
+
password = "#dbpass#";
43
+
database = cfg.database.name;
44
+
host = cfg.database.host;
45
+
port = cfg.database.port;
46
+
tls = cfg.database.tls;
49
+
server = cfg.settings.server or { } // {
50
+
bind = cfg.settings.server.bind or "localhost";
51
+
gopher_port = cfg.settings.server.gopher_port or 0;
52
+
autocert = !cfg.nginx.enable && cfg.acme.enable;
53
+
templates_parent_dir =
54
+
cfg.settings.server.templates_parent_dir or cfg.package.src;
55
+
static_parent_dir = cfg.settings.server.static_parent_dir or assets;
57
+
cfg.settings.server.pages_parent_dir or cfg.package.src;
58
+
keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
62
+
configFile = format.generate "config.ini" settings;
64
+
assets = pkgs.stdenvNoCC.mkDerivation {
65
+
pname = "writefreely-assets";
67
+
inherit (cfg.package) version src;
69
+
nativeBuildInputs = with pkgs.nodePackages; [ less ];
79
+
css_dir=$out/static/css
81
+
lessc $less_dir/app.less $css_dir/write.css
82
+
lessc $less_dir/fonts.less $css_dir/fonts.css
83
+
lessc $less_dir/icons.less $css_dir/icons.css
84
+
lessc $less_dir/prose.less $css_dir/prose.css
88
+
withConfigFile = text: ''
90
+
optionalString (cfg.database.passwordFile != null)
91
+
"$(head -n1 ${cfg.database.passwordFile})"
94
+
cp -f ${configFile} '${cfg.stateDir}/config.ini'
95
+
sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
96
+
chmod 440 '${cfg.stateDir}/config.ini'
104
+
local result=$(${config.services.mysql.package}/bin/mysql \
105
+
--user=${cfg.database.user} \
106
+
--password=$db_pass \
107
+
--database=${cfg.database.name} \
110
+
--skip-column-names \
123
+
local result=$(${sqlite}/bin/sqlite3 \
124
+
'${cfg.stateDir}/${settings.database.filename}'
134
+
options.services.writefreely = {
136
+
lib.mkEnableOption "Writefreely, build a digital writing community";
138
+
package = lib.mkOption {
139
+
type = lib.types.package;
140
+
default = pkgs.writefreely;
141
+
defaultText = lib.literalExpression "pkgs.writefreely";
142
+
description = "Writefreely package to use.";
145
+
stateDir = mkOption {
147
+
default = "/var/lib/writefreely";
148
+
description = "The state directory where keys and data are stored.";
153
+
default = "writefreely";
154
+
description = "User under which Writefreely is ran.";
159
+
default = "writefreely";
160
+
description = "Group under which Writefreely is ran.";
166
+
description = "The public host name to serve.";
167
+
example = "example.com";
170
+
settings = mkOption {
173
+
Writefreely configuration (<filename>config.ini</filename>). Refer to
174
+
<link xlink:href="https://writefreely.org/docs/latest/admin/config" />
178
+
type = types.submodule {
179
+
freeformType = format.type;
186
+
description = "The theme to apply.";
193
+
default = if cfg.nginx.enable then 18080 else 80;
194
+
defaultText = "80";
195
+
description = "The port WriteFreely should listen on.";
204
+
type = types.enum [ "sqlite3" "mysql" ];
205
+
default = "sqlite3";
206
+
description = "The database provider to use.";
211
+
default = "writefreely";
212
+
description = "The name of the database to store data in.";
216
+
type = types.nullOr types.str;
217
+
default = if cfg.database.type == "mysql" then "writefreely" else null;
218
+
defaultText = "writefreely";
219
+
description = "The database user to connect as.";
222
+
passwordFile = mkOption {
223
+
type = types.nullOr types.path;
225
+
description = "The file to load the database password from.";
230
+
default = "localhost";
231
+
description = "The database host to connect to.";
237
+
description = "The port used when connecting to the database host.";
244
+
"Whether or not TLS should be used for the database connection.";
247
+
migrate = mkOption {
251
+
"Whether or not to automatically run migrations on startup.";
254
+
createLocally = mkOption {
258
+
When <option>services.writefreely.database.type</option> is set to
259
+
<code>"mysql"</code>, this option will enable the MySQL service locally.
266
+
type = types.nullOr types.str;
267
+
description = "The name of the first admin user.";
271
+
initialPasswordFile = mkOption {
274
+
Path to a file containing the initial password for the admin user.
275
+
If not provided, the default password will be set to <code>nixos</code>.
277
+
default = pkgs.writeText "default-admin-pass" "nixos";
278
+
defaultText = "/nix/store/xxx-default-admin-pass";
283
+
enable = mkOption {
287
+
"Whether or not to enable and configure nginx as a proxy for WriteFreely.";
290
+
forceSSL = mkOption {
293
+
description = "Whether or not to force the use of SSL.";
298
+
enable = mkOption {
302
+
"Whether or not to automatically fetch and configure SSL certs.";
307
+
config = mkIf cfg.enable {
310
+
assertion = cfg.host != "";
311
+
message = "services.writefreely.host must be set";
314
+
assertion = isMysqlLocal -> cfg.database.passwordFile != null;
316
+
"services.writefreely.database.passwordFile must be set if services.writefreely.database.createLocally is set to true";
319
+
assertion = isSqlite -> !cfg.database.createLocally;
321
+
"services.writefreely.database.createLocally has no use when services.writefreely.database.type is set to sqlite3";
326
+
users = optionalAttrs (cfg.user == "writefreely") {
329
+
home = cfg.stateDir;
330
+
isSystemUser = true;
335
+
optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
338
+
systemd.tmpfiles.rules =
339
+
[ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ];
341
+
systemd.services.writefreely = {
342
+
after = [ "network.target" ]
343
+
++ optional isSqlite "writefreely-sqlite-init.service"
344
+
++ optional isMysql "writefreely-mysql-init.service"
345
+
++ optional isMysqlLocal "mysql.service";
346
+
wantedBy = [ "multi-user.target" ];
352
+
WorkingDirectory = cfg.stateDir;
353
+
Restart = "always";
356
+
"${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
357
+
AmbientCapabilities =
358
+
optionalString (settings.server.port < 1024) "cap_net_bind_service";
362
+
if ! test -d "${cfg.stateDir}/keys"; then
363
+
mkdir -p ${cfg.stateDir}/keys
365
+
# Key files end up with the wrong permissions by default.
366
+
# We need to correct them so that Writefreely can read them.
367
+
chmod -R 750 "${cfg.stateDir}/keys"
369
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
374
+
systemd.services.writefreely-sqlite-init = mkIf isSqlite {
375
+
wantedBy = [ "multi-user.target" ];
381
+
WorkingDirectory = cfg.stateDir;
382
+
ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
383
+
cfg.admin.initialPasswordFile;
387
+
migrateDatabase = optionalString cfg.database.migrate ''
388
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
391
+
createAdmin = optionalString (cfg.admin.name != null) ''
392
+
if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
393
+
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
395
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
399
+
if ! test -f '${settings.database.filename}'; then
400
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
409
+
systemd.services.writefreely-mysql-init = mkIf isMysql {
410
+
wantedBy = [ "multi-user.target" ];
411
+
after = optional isMysqlLocal "mysql.service";
417
+
WorkingDirectory = cfg.stateDir;
418
+
ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
419
+
++ optional (cfg.admin.initialPasswordFile != null)
420
+
cfg.admin.initialPasswordFile;
424
+
updateUser = optionalString isMysqlLocal ''
425
+
# WriteFreely currently *requires* a password for authentication, so we
426
+
# need to update the user in MySQL accordingly. By default MySQL users
427
+
# authenticate with auth_socket or unix_socket.
428
+
# See: https://github.com/writefreely/writefreely/issues/568
429
+
${config.services.mysql.package}/bin/mysql --skip-column-names --execute "ALTER USER '${cfg.database.user}'@'localhost' IDENTIFIED VIA unix_socket OR mysql_native_password USING PASSWORD('$db_pass'); FLUSH PRIVILEGES;"
432
+
migrateDatabase = optionalString cfg.database.migrate ''
433
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
436
+
createAdmin = optionalString (cfg.admin.name != null) ''
437
+
if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
438
+
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
439
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
445
+
if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
446
+
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
455
+
services.mysql = mkIf isMysqlLocal {
457
+
package = mkDefault pkgs.mariadb;
458
+
ensureDatabases = [ cfg.database.name ];
460
+
name = cfg.database.user;
461
+
ensurePermissions = {
462
+
"${cfg.database.name}.*" = "ALL PRIVILEGES";
463
+
# WriteFreely requires the use of passwords, so we need permissions
464
+
# to `ALTER` the user to add password support and also to reload
465
+
# permissions so they can be used.
466
+
"*.*" = "CREATE USER, RELOAD";
471
+
services.nginx = lib.mkIf cfg.nginx.enable {
473
+
recommendedProxySettings = true;
475
+
virtualHosts."${cfg.host}" = {
476
+
enableACME = cfg.acme.enable;
477
+
forceSSL = cfg.nginx.forceSSL;
480
+
proxyPass = "http://127.0.0.1:${toString settings.server.port}";