1 { config, lib, pkgs, ... }:
4 inherit (builtins) toString;
5 inherit (lib) types mkIf mkOption mkDefault;
6 inherit (lib) optional optionals optionalAttrs optionalString;
10 format = pkgs.formats.ini {
11 mkKeyValue = key: value:
13 value' = lib.optionalString (value != null)
14 (if builtins.isBool value then
15 if value == true then "true" else "false"
18 in "${key} = ${value'}";
21 cfg = config.services.writefreely;
23 isSqlite = cfg.database.type == "sqlite3";
24 isMysql = cfg.database.type == "mysql";
25 isMysqlLocal = isMysql && cfg.database.createLocally == true;
27 hostProtocol = if cfg.acme.enable then "https" else "http";
29 settings = cfg.settings // {
30 app = cfg.settings.app or { } // {
31 host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
34 database = if cfg.database.type == "sqlite3" then {
36 filename = cfg.settings.database.filename or "writefreely.db";
37 database = cfg.database.name;
40 username = cfg.database.user;
41 password = "#dbpass#";
42 database = cfg.database.name;
43 host = cfg.database.host;
44 port = cfg.database.port;
45 tls = cfg.database.tls;
48 server = cfg.settings.server or { } // {
49 bind = cfg.settings.server.bind or "localhost";
50 gopher_port = cfg.settings.server.gopher_port or 0;
51 autocert = !cfg.nginx.enable && cfg.acme.enable;
52 templates_parent_dir =
53 cfg.settings.server.templates_parent_dir or cfg.package.src;
54 static_parent_dir = cfg.settings.server.static_parent_dir or assets;
56 cfg.settings.server.pages_parent_dir or cfg.package.src;
57 keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
61 configFile = format.generate "config.ini" settings;
63 assets = pkgs.stdenvNoCC.mkDerivation {
64 pname = "writefreely-assets";
66 inherit (cfg.package) version src;
68 nativeBuildInputs = with pkgs.nodePackages; [ less ];
78 css_dir=$out/static/css
80 lessc $less_dir/app.less $css_dir/write.css
81 lessc $less_dir/fonts.less $css_dir/fonts.css
82 lessc $less_dir/icons.less $css_dir/icons.css
83 lessc $less_dir/prose.less $css_dir/prose.css
87 withConfigFile = text: ''
89 optionalString (cfg.database.passwordFile != null)
90 "$(head -n1 ${cfg.database.passwordFile})"
93 cp -f ${configFile} '${cfg.stateDir}/config.ini'
94 sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
95 chmod 440 '${cfg.stateDir}/config.ini'
103 local result=$(${config.services.mysql.package}/bin/mysql \
104 --user=${cfg.database.user} \
105 --password=$db_pass \
106 --database=${cfg.database.name} \
109 --skip-column-names \
122 local result=$(${sqlite}/bin/sqlite3 \
123 '${cfg.stateDir}/${settings.database.filename}' \
133 options.services.writefreely = {
135 lib.mkEnableOption "Writefreely, build a digital writing community";
137 package = lib.mkOption {
138 type = lib.types.package;
139 default = pkgs.writefreely;
140 defaultText = lib.literalExpression "pkgs.writefreely";
141 description = "Writefreely package to use.";
144 stateDir = mkOption {
146 default = "/var/lib/writefreely";
147 description = "The state directory where keys and data are stored.";
152 default = "writefreely";
153 description = "User under which Writefreely is ran.";
158 default = "writefreely";
159 description = "Group under which Writefreely is ran.";
165 description = "The public host name to serve.";
166 example = "example.com";
169 settings = mkOption {
172 Writefreely configuration ({file}`config.ini`). Refer to
173 <https://writefreely.org/docs/latest/admin/config>
177 type = types.submodule {
178 freeformType = format.type;
185 description = "The theme to apply.";
192 default = if cfg.nginx.enable then 18080 else 80;
194 description = "The port WriteFreely should listen on.";
203 type = types.enum [ "sqlite3" "mysql" ];
205 description = "The database provider to use.";
210 default = "writefreely";
211 description = "The name of the database to store data in.";
215 type = types.nullOr types.str;
216 default = if cfg.database.type == "mysql" then "writefreely" else null;
217 defaultText = "writefreely";
218 description = "The database user to connect as.";
221 passwordFile = mkOption {
222 type = types.nullOr types.path;
224 description = "The file to load the database password from.";
229 default = "localhost";
230 description = "The database host to connect to.";
236 description = "The port used when connecting to the database host.";
242 description = "Whether or not TLS should be used for the database connection.";
248 description = "Whether or not to automatically run migrations on startup.";
251 createLocally = mkOption {
255 When {option}`services.writefreely.database.type` is set to
256 `"mysql"`, this option will enable the MySQL service locally.
263 type = types.nullOr types.str;
264 description = "The name of the first admin user.";
268 initialPasswordFile = mkOption {
271 Path to a file containing the initial password for the admin user.
272 If not provided, the default password will be set to `nixos`.
274 default = pkgs.writeText "default-admin-pass" "nixos";
275 defaultText = "/nix/store/xxx-default-admin-pass";
283 description = "Whether or not to enable and configure nginx as a proxy for WriteFreely.";
286 forceSSL = mkOption {
289 description = "Whether or not to force the use of SSL.";
297 description = "Whether or not to automatically fetch and configure SSL certs.";
302 config = mkIf cfg.enable {
305 assertion = cfg.host != "";
306 message = "services.writefreely.host must be set";
309 assertion = isMysqlLocal -> cfg.database.passwordFile != null;
311 "services.writefreely.database.passwordFile must be set if services.writefreely.database.createLocally is set to true";
314 assertion = isSqlite -> !cfg.database.createLocally;
316 "services.writefreely.database.createLocally has no use when services.writefreely.database.type is set to sqlite3";
321 users = optionalAttrs (cfg.user == "writefreely") {
330 optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
333 systemd.tmpfiles.settings."10-writefreely".${cfg.stateDir}.d = {
334 inherit (cfg) user group;
338 systemd.services.writefreely = {
339 after = [ "network.target" ]
340 ++ optional isSqlite "writefreely-sqlite-init.service"
341 ++ optional isMysql "writefreely-mysql-init.service"
342 ++ optional isMysqlLocal "mysql.service";
343 wantedBy = [ "multi-user.target" ];
349 WorkingDirectory = cfg.stateDir;
353 "${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
354 AmbientCapabilities =
355 optionalString (settings.server.port < 1024) "cap_net_bind_service";
359 if ! test -d "${cfg.stateDir}/keys"; then
360 mkdir -p ${cfg.stateDir}/keys
362 # Key files end up with the wrong permissions by default.
363 # We need to correct them so that Writefreely can read them.
364 chmod -R 750 "${cfg.stateDir}/keys"
366 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
371 systemd.services.writefreely-sqlite-init = mkIf isSqlite {
372 wantedBy = [ "multi-user.target" ];
378 WorkingDirectory = cfg.stateDir;
379 ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
380 cfg.admin.initialPasswordFile;
384 migrateDatabase = optionalString cfg.database.migrate ''
385 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
388 createAdmin = optionalString (cfg.admin.name != null) ''
389 if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
390 admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
392 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
396 if ! test -f '${settings.database.filename}'; then
397 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
406 systemd.services.writefreely-mysql-init = mkIf isMysql {
407 wantedBy = [ "multi-user.target" ];
408 after = optional isMysqlLocal "mysql.service";
414 WorkingDirectory = cfg.stateDir;
415 ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
416 ++ optional (cfg.admin.initialPasswordFile != null)
417 cfg.admin.initialPasswordFile;
421 updateUser = optionalString isMysqlLocal ''
422 # WriteFreely currently *requires* a password for authentication, so we
423 # need to update the user in MySQL accordingly. By default MySQL users
424 # authenticate with auth_socket or unix_socket.
425 # See: https://github.com/writefreely/writefreely/issues/568
426 ${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;"
429 migrateDatabase = optionalString cfg.database.migrate ''
430 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
433 createAdmin = optionalString (cfg.admin.name != null) ''
434 if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
435 admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
436 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
442 if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
443 ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
452 services.mysql = mkIf isMysqlLocal {
454 package = mkDefault pkgs.mariadb;
455 ensureDatabases = [ cfg.database.name ];
457 name = cfg.database.user;
458 ensurePermissions = {
459 "${cfg.database.name}.*" = "ALL PRIVILEGES";
460 # WriteFreely requires the use of passwords, so we need permissions
461 # to `ALTER` the user to add password support and also to reload
462 # permissions so they can be used.
463 "*.*" = "CREATE USER, RELOAD";
468 services.nginx = lib.mkIf cfg.nginx.enable {
470 recommendedProxySettings = true;
472 virtualHosts."${cfg.host}" = {
473 enableACME = cfg.acme.enable;
474 forceSSL = cfg.nginx.forceSSL;
477 proxyPass = "http://127.0.0.1:${toString settings.server.port}";