forked from mirrors/nixpkgs
d98322834b
mostly no rendering changes except in buildkite, which used markdown where docbook was expected without marking up its markdown.
486 lines
14 KiB
Nix
486 lines
14 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
inherit (builtins) toString;
|
|
inherit (lib) types mkIf mkOption mkDefault;
|
|
inherit (lib) optional optionals optionalAttrs optionalString;
|
|
|
|
inherit (pkgs) sqlite;
|
|
|
|
format = pkgs.formats.ini {
|
|
mkKeyValue = key: value:
|
|
let
|
|
value' = if builtins.isNull value then
|
|
""
|
|
else if builtins.isBool value then
|
|
if value == true then "true" else "false"
|
|
else
|
|
toString value;
|
|
in "${key} = ${value'}";
|
|
};
|
|
|
|
cfg = config.services.writefreely;
|
|
|
|
isSqlite = cfg.database.type == "sqlite3";
|
|
isMysql = cfg.database.type == "mysql";
|
|
isMysqlLocal = isMysql && cfg.database.createLocally == true;
|
|
|
|
hostProtocol = if cfg.acme.enable then "https" else "http";
|
|
|
|
settings = cfg.settings // {
|
|
app = cfg.settings.app or { } // {
|
|
host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
|
|
};
|
|
|
|
database = if cfg.database.type == "sqlite3" then {
|
|
type = "sqlite3";
|
|
filename = cfg.settings.database.filename or "writefreely.db";
|
|
database = cfg.database.name;
|
|
} else {
|
|
type = "mysql";
|
|
username = cfg.database.user;
|
|
password = "#dbpass#";
|
|
database = cfg.database.name;
|
|
host = cfg.database.host;
|
|
port = cfg.database.port;
|
|
tls = cfg.database.tls;
|
|
};
|
|
|
|
server = cfg.settings.server or { } // {
|
|
bind = cfg.settings.server.bind or "localhost";
|
|
gopher_port = cfg.settings.server.gopher_port or 0;
|
|
autocert = !cfg.nginx.enable && cfg.acme.enable;
|
|
templates_parent_dir =
|
|
cfg.settings.server.templates_parent_dir or cfg.package.src;
|
|
static_parent_dir = cfg.settings.server.static_parent_dir or assets;
|
|
pages_parent_dir =
|
|
cfg.settings.server.pages_parent_dir or cfg.package.src;
|
|
keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
|
|
};
|
|
};
|
|
|
|
configFile = format.generate "config.ini" settings;
|
|
|
|
assets = pkgs.stdenvNoCC.mkDerivation {
|
|
pname = "writefreely-assets";
|
|
|
|
inherit (cfg.package) version src;
|
|
|
|
nativeBuildInputs = with pkgs.nodePackages; [ less ];
|
|
|
|
buildPhase = ''
|
|
mkdir -p $out
|
|
|
|
cp -r static $out/
|
|
'';
|
|
|
|
installPhase = ''
|
|
less_dir=$src/less
|
|
css_dir=$out/static/css
|
|
|
|
lessc $less_dir/app.less $css_dir/write.css
|
|
lessc $less_dir/fonts.less $css_dir/fonts.css
|
|
lessc $less_dir/icons.less $css_dir/icons.css
|
|
lessc $less_dir/prose.less $css_dir/prose.css
|
|
'';
|
|
};
|
|
|
|
withConfigFile = text: ''
|
|
db_pass=${
|
|
optionalString (cfg.database.passwordFile != null)
|
|
"$(head -n1 ${cfg.database.passwordFile})"
|
|
}
|
|
|
|
cp -f ${configFile} '${cfg.stateDir}/config.ini'
|
|
sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
|
|
chmod 440 '${cfg.stateDir}/config.ini'
|
|
|
|
${text}
|
|
'';
|
|
|
|
withMysql = text:
|
|
withConfigFile ''
|
|
query () {
|
|
local result=$(${config.services.mysql.package}/bin/mysql \
|
|
--user=${cfg.database.user} \
|
|
--password=$db_pass \
|
|
--database=${cfg.database.name} \
|
|
--silent \
|
|
--raw \
|
|
--skip-column-names \
|
|
--execute "$1" \
|
|
)
|
|
|
|
echo $result
|
|
}
|
|
|
|
${text}
|
|
'';
|
|
|
|
withSqlite = text:
|
|
withConfigFile ''
|
|
query () {
|
|
local result=$(${sqlite}/bin/sqlite3 \
|
|
'${cfg.stateDir}/${settings.database.filename}'
|
|
"$1" \
|
|
)
|
|
|
|
echo $result
|
|
}
|
|
|
|
${text}
|
|
'';
|
|
in {
|
|
options.services.writefreely = {
|
|
enable =
|
|
lib.mkEnableOption (lib.mdDoc "Writefreely, build a digital writing community");
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.writefreely;
|
|
defaultText = lib.literalExpression "pkgs.writefreely";
|
|
description = lib.mdDoc "Writefreely package to use.";
|
|
};
|
|
|
|
stateDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/writefreely";
|
|
description = lib.mdDoc "The state directory where keys and data are stored.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "writefreely";
|
|
description = lib.mdDoc "User under which Writefreely is ran.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "writefreely";
|
|
description = lib.mdDoc "Group under which Writefreely is ran.";
|
|
};
|
|
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = lib.mdDoc "The public host name to serve.";
|
|
example = "example.com";
|
|
};
|
|
|
|
settings = mkOption {
|
|
default = { };
|
|
description = lib.mdDoc ''
|
|
Writefreely configuration ({file}`config.ini`). Refer to
|
|
<https://writefreely.org/docs/latest/admin/config>
|
|
for details.
|
|
'';
|
|
|
|
type = types.submodule {
|
|
freeformType = format.type;
|
|
|
|
options = {
|
|
app = {
|
|
theme = mkOption {
|
|
type = types.str;
|
|
default = "write";
|
|
description = lib.mdDoc "The theme to apply.";
|
|
};
|
|
};
|
|
|
|
server = {
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = if cfg.nginx.enable then 18080 else 80;
|
|
defaultText = "80";
|
|
description = lib.mdDoc "The port WriteFreely should listen on.";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
database = {
|
|
type = mkOption {
|
|
type = types.enum [ "sqlite3" "mysql" ];
|
|
default = "sqlite3";
|
|
description = lib.mdDoc "The database provider to use.";
|
|
};
|
|
|
|
name = mkOption {
|
|
type = types.str;
|
|
default = "writefreely";
|
|
description = lib.mdDoc "The name of the database to store data in.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = if cfg.database.type == "mysql" then "writefreely" else null;
|
|
defaultText = "writefreely";
|
|
description = lib.mdDoc "The database user to connect as.";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
description = lib.mdDoc "The file to load the database password from.";
|
|
};
|
|
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = lib.mdDoc "The database host to connect to.";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 3306;
|
|
description = lib.mdDoc "The port used when connecting to the database host.";
|
|
};
|
|
|
|
tls = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description =
|
|
lib.mdDoc "Whether or not TLS should be used for the database connection.";
|
|
};
|
|
|
|
migrate = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description =
|
|
lib.mdDoc "Whether or not to automatically run migrations on startup.";
|
|
};
|
|
|
|
createLocally = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = lib.mdDoc ''
|
|
When {option}`services.writefreely.database.type` is set to
|
|
`"mysql"`, this option will enable the MySQL service locally.
|
|
'';
|
|
};
|
|
};
|
|
|
|
admin = {
|
|
name = mkOption {
|
|
type = types.nullOr types.str;
|
|
description = lib.mdDoc "The name of the first admin user.";
|
|
default = null;
|
|
};
|
|
|
|
initialPasswordFile = mkOption {
|
|
type = types.path;
|
|
description = lib.mdDoc ''
|
|
Path to a file containing the initial password for the admin user.
|
|
If not provided, the default password will be set to `nixos`.
|
|
'';
|
|
default = pkgs.writeText "default-admin-pass" "nixos";
|
|
defaultText = "/nix/store/xxx-default-admin-pass";
|
|
};
|
|
};
|
|
|
|
nginx = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description =
|
|
lib.mdDoc "Whether or not to enable and configure nginx as a proxy for WriteFreely.";
|
|
};
|
|
|
|
forceSSL = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = lib.mdDoc "Whether or not to force the use of SSL.";
|
|
};
|
|
};
|
|
|
|
acme = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description =
|
|
lib.mdDoc "Whether or not to automatically fetch and configure SSL certs.";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.host != "";
|
|
message = "services.writefreely.host must be set";
|
|
}
|
|
{
|
|
assertion = isMysqlLocal -> cfg.database.passwordFile != null;
|
|
message =
|
|
"services.writefreely.database.passwordFile must be set if services.writefreely.database.createLocally is set to true";
|
|
}
|
|
{
|
|
assertion = isSqlite -> !cfg.database.createLocally;
|
|
message =
|
|
"services.writefreely.database.createLocally has no use when services.writefreely.database.type is set to sqlite3";
|
|
}
|
|
];
|
|
|
|
users = {
|
|
users = optionalAttrs (cfg.user == "writefreely") {
|
|
writefreely = {
|
|
group = cfg.group;
|
|
home = cfg.stateDir;
|
|
isSystemUser = true;
|
|
};
|
|
};
|
|
|
|
groups =
|
|
optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
|
|
};
|
|
|
|
systemd.tmpfiles.rules =
|
|
[ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ];
|
|
|
|
systemd.services.writefreely = {
|
|
after = [ "network.target" ]
|
|
++ optional isSqlite "writefreely-sqlite-init.service"
|
|
++ optional isMysql "writefreely-mysql-init.service"
|
|
++ optional isMysqlLocal "mysql.service";
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.stateDir;
|
|
Restart = "always";
|
|
RestartSec = 20;
|
|
ExecStart =
|
|
"${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
|
|
AmbientCapabilities =
|
|
optionalString (settings.server.port < 1024) "cap_net_bind_service";
|
|
};
|
|
|
|
preStart = ''
|
|
if ! test -d "${cfg.stateDir}/keys"; then
|
|
mkdir -p ${cfg.stateDir}/keys
|
|
|
|
# Key files end up with the wrong permissions by default.
|
|
# We need to correct them so that Writefreely can read them.
|
|
chmod -R 750 "${cfg.stateDir}/keys"
|
|
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
|
|
fi
|
|
'';
|
|
};
|
|
|
|
systemd.services.writefreely-sqlite-init = mkIf isSqlite {
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.stateDir;
|
|
ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
|
|
cfg.admin.initialPasswordFile;
|
|
};
|
|
|
|
script = let
|
|
migrateDatabase = optionalString cfg.database.migrate ''
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
|
|
'';
|
|
|
|
createAdmin = optionalString (cfg.admin.name != null) ''
|
|
if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
|
|
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
|
|
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
|
|
fi
|
|
'';
|
|
in withSqlite ''
|
|
if ! test -f '${settings.database.filename}'; then
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
|
|
fi
|
|
|
|
${migrateDatabase}
|
|
|
|
${createAdmin}
|
|
'';
|
|
};
|
|
|
|
systemd.services.writefreely-mysql-init = mkIf isMysql {
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = optional isMysqlLocal "mysql.service";
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
WorkingDirectory = cfg.stateDir;
|
|
ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
|
|
++ optional (cfg.admin.initialPasswordFile != null)
|
|
cfg.admin.initialPasswordFile;
|
|
};
|
|
|
|
script = let
|
|
updateUser = optionalString isMysqlLocal ''
|
|
# WriteFreely currently *requires* a password for authentication, so we
|
|
# need to update the user in MySQL accordingly. By default MySQL users
|
|
# authenticate with auth_socket or unix_socket.
|
|
# See: https://github.com/writefreely/writefreely/issues/568
|
|
${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;"
|
|
'';
|
|
|
|
migrateDatabase = optionalString cfg.database.migrate ''
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
|
|
'';
|
|
|
|
createAdmin = optionalString (cfg.admin.name != null) ''
|
|
if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
|
|
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
|
|
fi
|
|
'';
|
|
in withMysql ''
|
|
${updateUser}
|
|
|
|
if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
|
|
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
|
|
fi
|
|
|
|
${migrateDatabase}
|
|
|
|
${createAdmin}
|
|
'';
|
|
};
|
|
|
|
services.mysql = mkIf isMysqlLocal {
|
|
enable = true;
|
|
package = mkDefault pkgs.mariadb;
|
|
ensureDatabases = [ cfg.database.name ];
|
|
ensureUsers = [{
|
|
name = cfg.database.user;
|
|
ensurePermissions = {
|
|
"${cfg.database.name}.*" = "ALL PRIVILEGES";
|
|
# WriteFreely requires the use of passwords, so we need permissions
|
|
# to `ALTER` the user to add password support and also to reload
|
|
# permissions so they can be used.
|
|
"*.*" = "CREATE USER, RELOAD";
|
|
};
|
|
}];
|
|
};
|
|
|
|
services.nginx = lib.mkIf cfg.nginx.enable {
|
|
enable = true;
|
|
recommendedProxySettings = true;
|
|
|
|
virtualHosts."${cfg.host}" = {
|
|
enableACME = cfg.acme.enable;
|
|
forceSSL = cfg.nginx.forceSSL;
|
|
|
|
locations."/" = {
|
|
proxyPass = "http://127.0.0.1:${toString settings.server.port}";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|