uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead
https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948 this can do it nicely. Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
commit
56de2bcd43
30691 changed files with 3076956 additions and 0 deletions
199
nixos/modules/services/web-apps/atlassian/confluence.nix
Normal file
199
nixos/modules/services/web-apps/atlassian/confluence.nix
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.confluence;
|
||||
|
||||
pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
|
||||
enableSSO = cfg.sso.enable;
|
||||
crowdProperties = ''
|
||||
application.name ${cfg.sso.applicationName}
|
||||
application.password ${cfg.sso.applicationPassword}
|
||||
application.login.url ${cfg.sso.crowd}/console/
|
||||
|
||||
crowd.server.url ${cfg.sso.crowd}/services/
|
||||
crowd.base.url ${cfg.sso.crowd}/
|
||||
|
||||
session.isauthenticated session.isauthenticated
|
||||
session.tokenkey session.tokenkey
|
||||
session.validationinterval ${toString cfg.sso.validationInterval}
|
||||
session.lastvalidation session.lastvalidation
|
||||
'';
|
||||
});
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.confluence = {
|
||||
enable = mkEnableOption "Atlassian Confluence service";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "confluence";
|
||||
description = "User which runs confluence.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "confluence";
|
||||
description = "Group which runs confluence.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/confluence";
|
||||
description = "Home directory of the confluence instance.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Address to listen on.";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 8090;
|
||||
description = "Port to listen on.";
|
||||
};
|
||||
|
||||
catalinaOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "-Xms1024m" "-Xmx2048m" "-Dconfluence.disable.peopledirectory.all=true" ];
|
||||
description = "Java options to pass to catalina/tomcat.";
|
||||
};
|
||||
|
||||
proxy = {
|
||||
enable = mkEnableOption "proxy support";
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
example = "confluence.example.com";
|
||||
description = "Virtual hostname at the proxy";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 443;
|
||||
example = 80;
|
||||
description = "Port used at the proxy";
|
||||
};
|
||||
|
||||
scheme = mkOption {
|
||||
type = types.str;
|
||||
default = "https";
|
||||
example = "http";
|
||||
description = "Protocol used at the proxy.";
|
||||
};
|
||||
};
|
||||
|
||||
sso = {
|
||||
enable = mkEnableOption "SSO with Atlassian Crowd";
|
||||
|
||||
crowd = mkOption {
|
||||
type = types.str;
|
||||
example = "http://localhost:8095/crowd";
|
||||
description = "Crowd Base URL without trailing slash";
|
||||
};
|
||||
|
||||
applicationName = mkOption {
|
||||
type = types.str;
|
||||
example = "jira";
|
||||
description = "Exact name of this Confluence instance in Crowd";
|
||||
};
|
||||
|
||||
applicationPassword = mkOption {
|
||||
type = types.str;
|
||||
description = "Application password of this Confluence instance in Crowd";
|
||||
};
|
||||
|
||||
validationInterval = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
example = 0;
|
||||
description = ''
|
||||
Set to 0, if you want authentication checks to occur on each
|
||||
request. Otherwise set to the number of minutes between request
|
||||
to validate if the user is logged in or out of the Crowd SSO
|
||||
server. Setting this value to 1 or higher will increase the
|
||||
performance of Crowd's integration.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.atlassian-confluence;
|
||||
defaultText = literalExpression "pkgs.atlassian-confluence";
|
||||
description = "Atlassian Confluence package to use.";
|
||||
};
|
||||
|
||||
jrePackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.oraclejre8;
|
||||
defaultText = literalExpression "pkgs.oraclejre8";
|
||||
description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.home}' - ${cfg.user} - - -"
|
||||
"d /run/confluence - - - - -"
|
||||
|
||||
"L+ /run/confluence/home - - - - ${cfg.home}"
|
||||
"L+ /run/confluence/logs - - - - ${cfg.home}/logs"
|
||||
"L+ /run/confluence/temp - - - - ${cfg.home}/temp"
|
||||
"L+ /run/confluence/work - - - - ${cfg.home}/work"
|
||||
"L+ /run/confluence/server.xml - - - - ${cfg.home}/server.xml"
|
||||
];
|
||||
|
||||
systemd.services.confluence = {
|
||||
description = "Atlassian Confluence";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
|
||||
path = [ cfg.jrePackage pkgs.bash ];
|
||||
|
||||
environment = {
|
||||
CONF_USER = cfg.user;
|
||||
JAVA_HOME = "${cfg.jrePackage}";
|
||||
CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.home}/{logs,work,temp,deploy}
|
||||
|
||||
sed -e 's,port="8090",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
|
||||
'' + (lib.optionalString cfg.proxy.enable ''
|
||||
-e 's,protocol="org.apache.coyote.http11.Http11NioProtocol",protocol="org.apache.coyote.http11.Http11NioProtocol" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}",' \
|
||||
'') + ''
|
||||
${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PrivateTmp = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10";
|
||||
ExecStart = "${pkg}/bin/start-confluence.sh -fg";
|
||||
ExecStop = "${pkg}/bin/stop-confluence.sh";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
166
nixos/modules/services/web-apps/atlassian/crowd.nix
Normal file
166
nixos/modules/services/web-apps/atlassian/crowd.nix
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.crowd;
|
||||
|
||||
pkg = cfg.package.override {
|
||||
home = cfg.home;
|
||||
port = cfg.listenPort;
|
||||
openidPassword = cfg.openidPassword;
|
||||
} // (optionalAttrs cfg.proxy.enable {
|
||||
proxyUrl = "${cfg.proxy.scheme}://${cfg.proxy.name}:${toString cfg.proxy.port}";
|
||||
});
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.crowd = {
|
||||
enable = mkEnableOption "Atlassian Crowd service";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "crowd";
|
||||
description = "User which runs Crowd.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "crowd";
|
||||
description = "Group which runs Crowd.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/crowd";
|
||||
description = "Home directory of the Crowd instance.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Address to listen on.";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 8092;
|
||||
description = "Port to listen on.";
|
||||
};
|
||||
|
||||
openidPassword = mkOption {
|
||||
type = types.str;
|
||||
description = "Application password for OpenID server.";
|
||||
};
|
||||
|
||||
catalinaOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "-Xms1024m" "-Xmx2048m" ];
|
||||
description = "Java options to pass to catalina/tomcat.";
|
||||
};
|
||||
|
||||
proxy = {
|
||||
enable = mkEnableOption "reverse proxy support";
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
example = "crowd.example.com";
|
||||
description = "Virtual hostname at the proxy";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 443;
|
||||
example = 80;
|
||||
description = "Port used at the proxy";
|
||||
};
|
||||
|
||||
scheme = mkOption {
|
||||
type = types.str;
|
||||
default = "https";
|
||||
example = "http";
|
||||
description = "Protocol used at the proxy.";
|
||||
};
|
||||
|
||||
secure = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether the connections to the proxy should be considered secure.";
|
||||
};
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.atlassian-crowd;
|
||||
defaultText = literalExpression "pkgs.atlassian-crowd";
|
||||
description = "Atlassian Crowd package to use.";
|
||||
};
|
||||
|
||||
jrePackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.oraclejre8;
|
||||
defaultText = literalExpression "pkgs.oraclejre8";
|
||||
description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.home}' - ${cfg.user} ${cfg.group} - -"
|
||||
"d /run/atlassian-crowd - - - - -"
|
||||
|
||||
"L+ /run/atlassian-crowd/database - - - - ${cfg.home}/database"
|
||||
"L+ /run/atlassian-crowd/logs - - - - ${cfg.home}/logs"
|
||||
"L+ /run/atlassian-crowd/work - - - - ${cfg.home}/work"
|
||||
"L+ /run/atlassian-crowd/server.xml - - - - ${cfg.home}/server.xml"
|
||||
];
|
||||
|
||||
systemd.services.atlassian-crowd = {
|
||||
description = "Atlassian Crowd";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
|
||||
path = [ cfg.jrePackage ];
|
||||
|
||||
environment = {
|
||||
JAVA_HOME = "${cfg.jrePackage}";
|
||||
CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
|
||||
CATALINA_TMPDIR = "/tmp";
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
rm -rf ${cfg.home}/work
|
||||
mkdir -p ${cfg.home}/{logs,database,work}
|
||||
|
||||
sed -e 's,port="8095",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
|
||||
'' + (lib.optionalString cfg.proxy.enable ''
|
||||
-e 's,compression="on",compression="off" protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${boolToString cfg.proxy.secure}",' \
|
||||
'') + ''
|
||||
${pkg}/apache-tomcat/conf/server.xml.dist > ${cfg.home}/server.xml
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PrivateTmp = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10";
|
||||
ExecStart = "${pkg}/start_crowd.sh -fg";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
207
nixos/modules/services/web-apps/atlassian/jira.nix
Normal file
207
nixos/modules/services/web-apps/atlassian/jira.nix
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.jira;
|
||||
|
||||
pkg = cfg.package.override (optionalAttrs cfg.sso.enable {
|
||||
enableSSO = cfg.sso.enable;
|
||||
crowdProperties = ''
|
||||
application.name ${cfg.sso.applicationName}
|
||||
application.password ${cfg.sso.applicationPassword}
|
||||
application.login.url ${cfg.sso.crowd}/console/
|
||||
|
||||
crowd.server.url ${cfg.sso.crowd}/services/
|
||||
crowd.base.url ${cfg.sso.crowd}/
|
||||
|
||||
session.isauthenticated session.isauthenticated
|
||||
session.tokenkey session.tokenkey
|
||||
session.validationinterval ${toString cfg.sso.validationInterval}
|
||||
session.lastvalidation session.lastvalidation
|
||||
'';
|
||||
});
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.jira = {
|
||||
enable = mkEnableOption "Atlassian JIRA service";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "jira";
|
||||
description = "User which runs JIRA.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "jira";
|
||||
description = "Group which runs JIRA.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/jira";
|
||||
description = "Home directory of the JIRA instance.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Address to listen on.";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 8091;
|
||||
description = "Port to listen on.";
|
||||
};
|
||||
|
||||
catalinaOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "-Xms1024m" "-Xmx2048m" ];
|
||||
description = "Java options to pass to catalina/tomcat.";
|
||||
};
|
||||
|
||||
proxy = {
|
||||
enable = mkEnableOption "reverse proxy support";
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
example = "jira.example.com";
|
||||
description = "Virtual hostname at the proxy";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 443;
|
||||
example = 80;
|
||||
description = "Port used at the proxy";
|
||||
};
|
||||
|
||||
scheme = mkOption {
|
||||
type = types.str;
|
||||
default = "https";
|
||||
example = "http";
|
||||
description = "Protocol used at the proxy.";
|
||||
};
|
||||
|
||||
secure = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether the connections to the proxy should be considered secure.";
|
||||
};
|
||||
};
|
||||
|
||||
sso = {
|
||||
enable = mkEnableOption "SSO with Atlassian Crowd";
|
||||
|
||||
crowd = mkOption {
|
||||
type = types.str;
|
||||
example = "http://localhost:8095/crowd";
|
||||
description = "Crowd Base URL without trailing slash";
|
||||
};
|
||||
|
||||
applicationName = mkOption {
|
||||
type = types.str;
|
||||
example = "jira";
|
||||
description = "Exact name of this JIRA instance in Crowd";
|
||||
};
|
||||
|
||||
applicationPassword = mkOption {
|
||||
type = types.str;
|
||||
description = "Application password of this JIRA instance in Crowd";
|
||||
};
|
||||
|
||||
validationInterval = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
example = 0;
|
||||
description = ''
|
||||
Set to 0, if you want authentication checks to occur on each
|
||||
request. Otherwise set to the number of minutes between request
|
||||
to validate if the user is logged in or out of the Crowd SSO
|
||||
server. Setting this value to 1 or higher will increase the
|
||||
performance of Crowd's integration.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.atlassian-jira;
|
||||
defaultText = literalExpression "pkgs.atlassian-jira";
|
||||
description = "Atlassian JIRA package to use.";
|
||||
};
|
||||
|
||||
jrePackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.oraclejre8;
|
||||
defaultText = literalExpression "pkgs.oraclejre8";
|
||||
description = "Note that Atlassian only support the Oracle JRE (JRASERVER-46152).";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.home;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.home}' - ${cfg.user} - - -"
|
||||
"d /run/atlassian-jira - - - - -"
|
||||
|
||||
"L+ /run/atlassian-jira/home - - - - ${cfg.home}"
|
||||
"L+ /run/atlassian-jira/logs - - - - ${cfg.home}/logs"
|
||||
"L+ /run/atlassian-jira/work - - - - ${cfg.home}/work"
|
||||
"L+ /run/atlassian-jira/temp - - - - ${cfg.home}/temp"
|
||||
"L+ /run/atlassian-jira/server.xml - - - - ${cfg.home}/server.xml"
|
||||
];
|
||||
|
||||
systemd.services.atlassian-jira = {
|
||||
description = "Atlassian JIRA";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
|
||||
path = [ cfg.jrePackage pkgs.bash ];
|
||||
|
||||
environment = {
|
||||
JIRA_USER = cfg.user;
|
||||
JIRA_HOME = cfg.home;
|
||||
JAVA_HOME = "${cfg.jrePackage}";
|
||||
CATALINA_OPTS = concatStringsSep " " cfg.catalinaOptions;
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.home}/{logs,work,temp,deploy}
|
||||
|
||||
sed -e 's,port="8080",port="${toString cfg.listenPort}" address="${cfg.listenAddress}",' \
|
||||
'' + (lib.optionalString cfg.proxy.enable ''
|
||||
-e 's,protocol="HTTP/1.1",protocol="HTTP/1.1" proxyName="${cfg.proxy.name}" proxyPort="${toString cfg.proxy.port}" scheme="${cfg.proxy.scheme}" secure="${toString cfg.proxy.secure}",' \
|
||||
'') + ''
|
||||
${pkg}/conf/server.xml.dist > ${cfg.home}/server.xml
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PrivateTmp = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = "10";
|
||||
ExecStart = "${pkg}/bin/start-jira.sh -fg";
|
||||
ExecStop = "${pkg}/bin/stop-jira.sh";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
170
nixos/modules/services/web-apps/baget.nix
Normal file
170
nixos/modules/services/web-apps/baget.nix
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.baget;
|
||||
|
||||
defaultConfig = {
|
||||
"PackageDeletionBehavior" = "Unlist";
|
||||
"AllowPackageOverwrites" = false;
|
||||
|
||||
"Database" = {
|
||||
"Type" = "Sqlite";
|
||||
"ConnectionString" = "Data Source=baget.db";
|
||||
};
|
||||
|
||||
"Storage" = {
|
||||
"Type" = "FileSystem";
|
||||
"Path" = "";
|
||||
};
|
||||
|
||||
"Search" = {
|
||||
"Type" = "Database";
|
||||
};
|
||||
|
||||
"Mirror" = {
|
||||
"Enabled" = false;
|
||||
"PackageSource" = "https://api.nuget.org/v3/index.json";
|
||||
};
|
||||
|
||||
"Logging" = {
|
||||
"IncludeScopes" = false;
|
||||
"Debug" = {
|
||||
"LogLevel" = {
|
||||
"Default" = "Warning";
|
||||
};
|
||||
};
|
||||
"Console" = {
|
||||
"LogLevel" = {
|
||||
"Microsoft.Hosting.Lifetime" = "Information";
|
||||
"Default" = "Warning";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
configAttrs = recursiveUpdate defaultConfig cfg.extraConfig;
|
||||
|
||||
configFormat = pkgs.formats.json {};
|
||||
configFile = configFormat.generate "appsettings.json" configAttrs;
|
||||
|
||||
in
|
||||
{
|
||||
options.services.baget = {
|
||||
enable = mkEnableOption "BaGet NuGet-compatible server";
|
||||
|
||||
apiKeyFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/root/baget.key";
|
||||
description = ''
|
||||
Private API key for BaGet.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = configFormat.type;
|
||||
default = {};
|
||||
example = {
|
||||
"Database" = {
|
||||
"Type" = "PostgreSql";
|
||||
"ConnectionString" = "Server=/run/postgresql;Port=5432;";
|
||||
};
|
||||
};
|
||||
defaultText = literalExpression ''
|
||||
{
|
||||
"PackageDeletionBehavior" = "Unlist";
|
||||
"AllowPackageOverwrites" = false;
|
||||
|
||||
"Database" = {
|
||||
"Type" = "Sqlite";
|
||||
"ConnectionString" = "Data Source=baget.db";
|
||||
};
|
||||
|
||||
"Storage" = {
|
||||
"Type" = "FileSystem";
|
||||
"Path" = "";
|
||||
};
|
||||
|
||||
"Search" = {
|
||||
"Type" = "Database";
|
||||
};
|
||||
|
||||
"Mirror" = {
|
||||
"Enabled" = false;
|
||||
"PackageSource" = "https://api.nuget.org/v3/index.json";
|
||||
};
|
||||
|
||||
"Logging" = {
|
||||
"IncludeScopes" = false;
|
||||
"Debug" = {
|
||||
"LogLevel" = {
|
||||
"Default" = "Warning";
|
||||
};
|
||||
};
|
||||
"Console" = {
|
||||
"LogLevel" = {
|
||||
"Microsoft.Hosting.Lifetime" = "Information";
|
||||
"Default" = "Warning";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Extra configuration options for BaGet. Refer to <link xlink:href="https://loic-sharma.github.io/BaGet/configuration/"/> for details.
|
||||
Default value is merged with values from here.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
systemd.services.baget = {
|
||||
description = "BaGet server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network.target" "network-online.target" ];
|
||||
path = [ pkgs.jq ];
|
||||
serviceConfig = {
|
||||
WorkingDirectory = "/var/lib/baget";
|
||||
DynamicUser = true;
|
||||
StateDirectory = "baget";
|
||||
StateDirectoryMode = "0700";
|
||||
LoadCredential = "api_key:${cfg.apiKeyFile}";
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
PrivateMounts = true;
|
||||
ProtectHome = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProcSubset = "pid";
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHostname = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" ];
|
||||
};
|
||||
script = ''
|
||||
jq --slurpfile apiKeys <(jq -R . "$CREDENTIALS_DIRECTORY/api_key") '.ApiKey = $apiKeys[0]' ${configFile} > appsettings.json
|
||||
ln -snf ${pkgs.baget}/lib/BaGet/wwwroot wwwroot
|
||||
exec ${pkgs.baget}/bin/BaGet
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
449
nixos/modules/services/web-apps/bookstack.nix
Normal file
449
nixos/modules/services/web-apps/bookstack.nix
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.bookstack;
|
||||
bookstack = pkgs.bookstack.override {
|
||||
dataDir = cfg.dataDir;
|
||||
};
|
||||
db = cfg.database;
|
||||
mail = cfg.mail;
|
||||
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
|
||||
# shell script for local administration
|
||||
artisan = pkgs.writeScriptBin "bookstack" ''
|
||||
#! ${pkgs.runtimeShell}
|
||||
cd ${bookstack}
|
||||
sudo=exec
|
||||
if [[ "$USER" != ${user} ]]; then
|
||||
sudo='exec /run/wrappers/bin/sudo -u ${user}'
|
||||
fi
|
||||
$sudo ${pkgs.php}/bin/php artisan $*
|
||||
'';
|
||||
|
||||
tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
|
||||
|
||||
in {
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "services" "bookstack" "extraConfig" ] "Use services.bookstack.config instead.")
|
||||
(mkRemovedOptionModule [ "services" "bookstack" "cacheDir" ] "The cache directory is now handled automatically.")
|
||||
];
|
||||
|
||||
options.services.bookstack = {
|
||||
|
||||
enable = mkEnableOption "BookStack";
|
||||
|
||||
user = mkOption {
|
||||
default = "bookstack";
|
||||
description = "User bookstack runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
default = "bookstack";
|
||||
description = "Group bookstack runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
appKeyFile = mkOption {
|
||||
description = ''
|
||||
A file containing the Laravel APP_KEY - a 32 character long,
|
||||
base64 encoded key used for encryption where needed. Can be
|
||||
generated with <code>head -c 32 /dev/urandom | base64</code>.
|
||||
'';
|
||||
example = "/run/keys/bookstack-appkey";
|
||||
type = types.path;
|
||||
};
|
||||
|
||||
hostname = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if config.networking.domain != null then
|
||||
config.networking.fqdn
|
||||
else
|
||||
config.networking.hostName;
|
||||
defaultText = lib.literalExpression "config.networking.fqdn";
|
||||
example = "bookstack.example.com";
|
||||
description = ''
|
||||
The hostname to serve BookStack on.
|
||||
'';
|
||||
};
|
||||
|
||||
appURL = mkOption {
|
||||
description = ''
|
||||
The root URL that you want to host BookStack on. All URLs in BookStack will be generated using this value.
|
||||
If you change this in the future you may need to run a command to update stored URLs in the database. Command example: <code>php artisan bookstack:update-url https://old.example.com https://new.example.com</code>
|
||||
'';
|
||||
default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}";
|
||||
defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}'';
|
||||
example = "https://example.com";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
description = "BookStack data directory";
|
||||
default = "/var/lib/bookstack";
|
||||
type = types.path;
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = "Database host port.";
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "bookstack";
|
||||
description = "Database name.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = user;
|
||||
defaultText = literalExpression "user";
|
||||
description = "Database username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/bookstack-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
mail = {
|
||||
driver = mkOption {
|
||||
type = types.enum [ "smtp" "sendmail" ];
|
||||
default = "smtp";
|
||||
description = "Mail driver to use.";
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Mail host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 1025;
|
||||
description = "Mail host port.";
|
||||
};
|
||||
fromName = mkOption {
|
||||
type = types.str;
|
||||
default = "BookStack";
|
||||
description = "Mail \"from\" name.";
|
||||
};
|
||||
from = mkOption {
|
||||
type = types.str;
|
||||
default = "mail@bookstackapp.com";
|
||||
description = "Mail \"from\" email.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "bookstack";
|
||||
description = "Mail username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/bookstack-mailpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>mail.user</option>.
|
||||
'';
|
||||
};
|
||||
encryption = mkOption {
|
||||
type = with types; nullOr (enum [ "tls" ]);
|
||||
default = null;
|
||||
description = "SMTP encryption mechanism to use.";
|
||||
};
|
||||
};
|
||||
|
||||
maxUploadSize = mkOption {
|
||||
type = types.str;
|
||||
default = "18M";
|
||||
example = "1G";
|
||||
description = "The maximum size for uploads (e.g. images).";
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the bookstack PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
type = types.submodule (
|
||||
recursiveUpdate
|
||||
(import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
|
||||
);
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
serverAliases = [
|
||||
"bookstack.''${config.networking.domain}"
|
||||
];
|
||||
# To enable encryption and let let's encrypt take care of certificate
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
With this option, you can customize the nginx virtualHost settings.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types;
|
||||
attrsOf
|
||||
(nullOr
|
||||
(either
|
||||
(oneOf [
|
||||
bool
|
||||
int
|
||||
port
|
||||
path
|
||||
str
|
||||
])
|
||||
(submodule {
|
||||
options = {
|
||||
_secret = mkOption {
|
||||
type = nullOr str;
|
||||
description = ''
|
||||
The path to a file containing the value the
|
||||
option should be set to in the final
|
||||
configuration file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
})));
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
ALLOWED_IFRAME_HOSTS = "https://example.com";
|
||||
WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
|
||||
AUTH_METHOD = "oidc";
|
||||
OIDC_NAME = "MyLogin";
|
||||
OIDC_DISPLAY_NAME_CLAIMS = "name";
|
||||
OIDC_CLIENT_ID = "bookstack";
|
||||
OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
|
||||
OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
|
||||
OIDC_ISSUER_DISCOVER = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
BookStack configuration options to set in the
|
||||
<filename>.env</filename> file.
|
||||
|
||||
Refer to <link xlink:href="https://www.bookstackapp.com/docs/"/>
|
||||
for details on supported values.
|
||||
|
||||
Settings containing secret data should be set to an attribute
|
||||
set containing the attribute <literal>_secret</literal> - a
|
||||
string pointing to a file containing the value the option
|
||||
should be set to. See the example to get a better picture of
|
||||
this: in the resulting <filename>.env</filename> file, the
|
||||
<literal>OIDC_CLIENT_SECRET</literal> key will be set to the
|
||||
contents of the <filename>/run/keys/oidc_secret</filename>
|
||||
file.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = db.createLocally -> db.user == user;
|
||||
message = "services.bookstack.database.user must be set to ${user} if services.bookstack.database.createLocally is set true.";
|
||||
}
|
||||
{ assertion = db.createLocally -> db.passwordFile == null;
|
||||
message = "services.bookstack.database.passwordFile cannot be specified if services.bookstack.database.createLocally is set to true.";
|
||||
}
|
||||
];
|
||||
|
||||
services.bookstack.config = {
|
||||
APP_KEY._secret = cfg.appKeyFile;
|
||||
APP_URL = cfg.appURL;
|
||||
DB_HOST = db.host;
|
||||
DB_PORT = db.port;
|
||||
DB_DATABASE = db.name;
|
||||
DB_USERNAME = db.user;
|
||||
MAIL_DRIVER = mail.driver;
|
||||
MAIL_FROM_NAME = mail.fromName;
|
||||
MAIL_FROM = mail.from;
|
||||
MAIL_HOST = mail.host;
|
||||
MAIL_PORT = mail.port;
|
||||
MAIL_USERNAME = mail.user;
|
||||
MAIL_ENCRYPTION = mail.encryption;
|
||||
DB_PASSWORD._secret = db.passwordFile;
|
||||
MAIL_PASSWORD._secret = mail.passwordFile;
|
||||
APP_SERVICES_CACHE = "/run/bookstack/cache/services.php";
|
||||
APP_PACKAGES_CACHE = "/run/bookstack/cache/packages.php";
|
||||
APP_CONFIG_CACHE = "/run/bookstack/cache/config.php";
|
||||
APP_ROUTES_CACHE = "/run/bookstack/cache/routes-v7.php";
|
||||
APP_EVENTS_CACHE = "/run/bookstack/cache/events.php";
|
||||
SESSION_SECURE_COOKIE = tlsEnabled;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ artisan ];
|
||||
|
||||
services.mysql = mkIf db.createLocally {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ db.name ];
|
||||
ensureUsers = [
|
||||
{ name = db.user;
|
||||
ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.bookstack = {
|
||||
inherit user;
|
||||
inherit group;
|
||||
phpOptions = ''
|
||||
log_errors = on
|
||||
post_max_size = ${cfg.maxUploadSize}
|
||||
upload_max_filesize = ${cfg.maxUploadSize}
|
||||
'';
|
||||
settings = {
|
||||
"listen.mode" = "0660";
|
||||
"listen.owner" = user;
|
||||
"listen.group" = group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = mkDefault true;
|
||||
recommendedTlsSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedGzipSettings = true;
|
||||
virtualHosts.${cfg.hostname} = mkMerge [ cfg.nginx {
|
||||
root = mkForce "${bookstack}/public";
|
||||
locations = {
|
||||
"/" = {
|
||||
index = "index.php";
|
||||
tryFiles = "$uri $uri/ /index.php?$query_string";
|
||||
};
|
||||
"~ \.php$".extraConfig = ''
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools."bookstack".socket};
|
||||
'';
|
||||
"~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
|
||||
extraConfig = "expires 365d;";
|
||||
};
|
||||
};
|
||||
}];
|
||||
};
|
||||
|
||||
systemd.services.bookstack-setup = {
|
||||
description = "Preperation tasks for BookStack";
|
||||
before = [ "phpfpm-bookstack.service" ];
|
||||
after = optional db.createLocally "mysql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = user;
|
||||
WorkingDirectory = "${bookstack}";
|
||||
RuntimeDirectory = "bookstack/cache";
|
||||
RuntimeDirectoryMode = 0700;
|
||||
};
|
||||
path = [ pkgs.replace-secret ];
|
||||
script =
|
||||
let
|
||||
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
|
||||
bookstackEnvVars = lib.generators.toKeyValue {
|
||||
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
||||
mkValueString = v: with builtins;
|
||||
if isInt v then toString v
|
||||
else if isString v then v
|
||||
else if true == v then "true"
|
||||
else if false == v then "false"
|
||||
else if isSecret v then hashString "sha256" v._secret
|
||||
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
||||
};
|
||||
};
|
||||
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
|
||||
mkSecretReplacement = file: ''
|
||||
replace-secret ${escapeShellArgs [ (builtins.hashString "sha256" file) file "${cfg.dataDir}/.env" ]}
|
||||
'';
|
||||
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
||||
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
|
||||
bookstackEnv = pkgs.writeText "bookstack.env" (bookstackEnvVars filteredConfig);
|
||||
in ''
|
||||
# error handling
|
||||
set -euo pipefail
|
||||
|
||||
# set permissions
|
||||
umask 077
|
||||
|
||||
# create .env file
|
||||
install -T -m 0600 -o ${user} ${bookstackEnv} "${cfg.dataDir}/.env"
|
||||
${secretReplacements}
|
||||
if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
|
||||
sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
|
||||
fi
|
||||
|
||||
# migrate db
|
||||
${pkgs.php}/bin/php artisan migrate --force
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/public 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -"
|
||||
];
|
||||
|
||||
users = {
|
||||
users = mkIf (user == "bookstack") {
|
||||
bookstack = {
|
||||
inherit group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
"${config.services.nginx.user}".extraGroups = [ group ];
|
||||
};
|
||||
groups = mkIf (group == "bookstack") {
|
||||
bookstack = {};
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ ymarkus ];
|
||||
}
|
||||
165
nixos/modules/services/web-apps/calibre-web.nix
Normal file
165
nixos/modules/services/web-apps/calibre-web.nix
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.calibre-web;
|
||||
|
||||
inherit (lib) concatStringsSep mkEnableOption mkIf mkOption optional optionalString types;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.calibre-web = {
|
||||
enable = mkEnableOption "Calibre-Web";
|
||||
|
||||
listen = {
|
||||
ip = mkOption {
|
||||
type = types.str;
|
||||
default = "::1";
|
||||
description = ''
|
||||
IP address that Calibre-Web should listen on.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8083;
|
||||
description = ''
|
||||
Listen port for Calibre-Web.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "calibre-web";
|
||||
description = ''
|
||||
The directory below <filename>/var/lib</filename> where Calibre-Web stores its data.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "calibre-web";
|
||||
description = "User account under which Calibre-Web runs.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "calibre-web";
|
||||
description = "Group account under which Calibre-Web runs.";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Open ports in the firewall for the server.
|
||||
'';
|
||||
};
|
||||
|
||||
options = {
|
||||
calibreLibrary = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to Calibre library.
|
||||
'';
|
||||
};
|
||||
|
||||
enableBookConversion = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Configure path to the Calibre's ebook-convert in the DB.
|
||||
'';
|
||||
};
|
||||
|
||||
enableBookUploading = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Allow books to be uploaded via Calibre-Web UI.
|
||||
'';
|
||||
};
|
||||
|
||||
reverseProxyAuth = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable authorization using auth proxy.
|
||||
'';
|
||||
};
|
||||
|
||||
header = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
Auth proxy header name.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.calibre-web = let
|
||||
appDb = "/var/lib/${cfg.dataDir}/app.db";
|
||||
gdriveDb = "/var/lib/${cfg.dataDir}/gdrive.db";
|
||||
calibreWebCmd = "${pkgs.calibre-web}/bin/calibre-web -p ${appDb} -g ${gdriveDb}";
|
||||
|
||||
settings = concatStringsSep ", " (
|
||||
[
|
||||
"config_port = ${toString cfg.listen.port}"
|
||||
"config_uploading = ${if cfg.options.enableBookUploading then "1" else "0"}"
|
||||
"config_allow_reverse_proxy_header_login = ${if cfg.options.reverseProxyAuth.enable then "1" else "0"}"
|
||||
"config_reverse_proxy_login_header_name = '${cfg.options.reverseProxyAuth.header}'"
|
||||
]
|
||||
++ optional (cfg.options.calibreLibrary != null) "config_calibre_dir = '${cfg.options.calibreLibrary}'"
|
||||
++ optional cfg.options.enableBookConversion "config_converterpath = '${pkgs.calibre}/bin/ebook-convert'"
|
||||
);
|
||||
in
|
||||
{
|
||||
description = "Web app for browsing, reading and downloading eBooks stored in a Calibre database";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
|
||||
StateDirectory = cfg.dataDir;
|
||||
ExecStartPre = pkgs.writeShellScript "calibre-web-pre-start" (
|
||||
''
|
||||
__RUN_MIGRATIONS_AND_EXIT=1 ${calibreWebCmd}
|
||||
|
||||
${pkgs.sqlite}/bin/sqlite3 ${appDb} "update settings set ${settings}"
|
||||
'' + optionalString (cfg.options.calibreLibrary != null) ''
|
||||
test -f ${cfg.options.calibreLibrary}/metadata.db || { echo "Invalid Calibre library"; exit 1; }
|
||||
''
|
||||
);
|
||||
|
||||
ExecStart = "${calibreWebCmd} -i ${cfg.listen.ip}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.listen.port ];
|
||||
};
|
||||
|
||||
users.users = mkIf (cfg.user == "calibre-web") {
|
||||
calibre-web = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = mkIf (cfg.group == "calibre-web") {
|
||||
calibre-web = {};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ pborzenkov ];
|
||||
}
|
||||
139
nixos/modules/services/web-apps/code-server.nix
Normal file
139
nixos/modules/services/web-apps/code-server.nix
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
|
||||
cfg = config.services.code-server;
|
||||
defaultUser = "code-server";
|
||||
defaultGroup = defaultUser;
|
||||
|
||||
in {
|
||||
###### interface
|
||||
options = {
|
||||
services.code-server = {
|
||||
enable = mkEnableOption "code-server";
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.code-server;
|
||||
defaultText = "pkgs.code-server";
|
||||
description = "Which code-server derivation to use.";
|
||||
type = types.package;
|
||||
};
|
||||
|
||||
extraPackages = mkOption {
|
||||
default = [ ];
|
||||
description = "Packages that are available in the PATH of code-server.";
|
||||
example = "[ pkgs.go ]";
|
||||
type = types.listOf types.package;
|
||||
};
|
||||
|
||||
extraEnvironment = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
description =
|
||||
"Additional environment variables to passed to code-server.";
|
||||
default = { };
|
||||
example = { PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig"; };
|
||||
};
|
||||
|
||||
extraArguments = mkOption {
|
||||
default = [ "--disable-telemetry" ];
|
||||
description = "Additional arguments that passed to code-server";
|
||||
example = ''[ "--verbose" ]'';
|
||||
type = types.listOf types.str;
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
default = "127.0.0.1";
|
||||
description = "The host-ip to bind to.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
default = 4444;
|
||||
description = "The port where code-server runs.";
|
||||
type = types.port;
|
||||
};
|
||||
|
||||
auth = mkOption {
|
||||
default = "password";
|
||||
description = "The type of authentication to use.";
|
||||
type = types.enum [ "none" "password" ];
|
||||
};
|
||||
|
||||
hashedPassword = mkOption {
|
||||
default = "";
|
||||
description =
|
||||
"Create the password with: 'echo -n 'thisismypassword' | npx argon2-cli -e'.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
default = defaultUser;
|
||||
example = "yourUser";
|
||||
description = ''
|
||||
The user to run code-server as.
|
||||
By default, a user named <literal>${defaultUser}</literal> will be created.
|
||||
'';
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
default = defaultGroup;
|
||||
example = "yourGroup";
|
||||
description = ''
|
||||
The group to run code-server under.
|
||||
By default, a group named <literal>${defaultGroup}</literal> will be created.
|
||||
'';
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
extraGroups = mkOption {
|
||||
default = [ ];
|
||||
description =
|
||||
"An array of additional groups for the <literal>${defaultUser}</literal> user.";
|
||||
example = [ "docker" ];
|
||||
type = types.listOf types.str;
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
###### implementation
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.code-server = {
|
||||
description = "VSCode server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
path = cfg.extraPackages;
|
||||
environment = {
|
||||
HASHED_PASSWORD = cfg.hashedPassword;
|
||||
} // cfg.extraEnvironment;
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/code-server --bind-addr ${cfg.host}:${toString cfg.port} --auth ${cfg.auth} " + builtins.concatStringsSep " " cfg.extraArguments;
|
||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||
RuntimeDirectory = cfg.user;
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
users.users."${cfg.user}" = mkMerge [
|
||||
(mkIf (cfg.user == defaultUser) {
|
||||
isNormalUser = true;
|
||||
description = "code-server user";
|
||||
inherit (cfg) group;
|
||||
})
|
||||
{
|
||||
packages = cfg.extraPackages;
|
||||
inherit (cfg) extraGroups;
|
||||
}
|
||||
];
|
||||
|
||||
users.groups."${defaultGroup}" = mkIf (cfg.group == defaultGroup) { };
|
||||
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ stackshadow ];
|
||||
}
|
||||
72
nixos/modules/services/web-apps/convos.nix
Normal file
72
nixos/modules/services/web-apps/convos.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.convos;
|
||||
in
|
||||
{
|
||||
options.services.convos = {
|
||||
enable = mkEnableOption "Convos";
|
||||
listenPort = mkOption {
|
||||
type = types.port;
|
||||
default = 3000;
|
||||
example = 8080;
|
||||
description = "Port the web interface should listen on";
|
||||
};
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "*";
|
||||
example = "127.0.0.1";
|
||||
description = "Address or host the web interface should listen on";
|
||||
};
|
||||
reverseProxy = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enables reverse proxy support. This will allow Convos to automatically
|
||||
pick up the <literal>X-Forwarded-For</literal> and
|
||||
<literal>X-Request-Base</literal> HTTP headers set in your reverse proxy
|
||||
web server. Note that enabling this option without a reverse proxy in
|
||||
front will be a security issue.
|
||||
'';
|
||||
};
|
||||
};
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.convos = {
|
||||
description = "Convos Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
environment = {
|
||||
CONVOS_HOME = "%S/convos";
|
||||
CONVOS_REVERSE_PROXY = if cfg.reverseProxy then "1" else "0";
|
||||
MOJO_LISTEN = "http://${toString cfg.listenAddress}:${toString cfg.listenPort}";
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.convos}/bin/convos daemon";
|
||||
Restart = "on-failure";
|
||||
StateDirectory = "convos";
|
||||
WorkingDirectory = "%S/convos";
|
||||
DynamicUser = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
ProtectHome = true;
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateUsers = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6"];
|
||||
SystemCallFilter = "@system-service";
|
||||
SystemCallArchitectures = "native";
|
||||
CapabilityBoundingSet = "";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
54
nixos/modules/services/web-apps/cryptpad.nix
Normal file
54
nixos/modules/services/web-apps/cryptpad.nix
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.cryptpad;
|
||||
in
|
||||
{
|
||||
options.services.cryptpad = {
|
||||
enable = mkEnableOption "the Cryptpad service";
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.cryptpad;
|
||||
defaultText = literalExpression "pkgs.cryptpad";
|
||||
type = types.package;
|
||||
description = "
|
||||
Cryptpad package to use.
|
||||
";
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.path;
|
||||
default = "${cfg.package}/lib/node_modules/cryptpad/config/config.example.js";
|
||||
defaultText = literalExpression ''"''${package}/lib/node_modules/cryptpad/config/config.example.js"'';
|
||||
description = ''
|
||||
Path to the JavaScript configuration file.
|
||||
|
||||
See <link
|
||||
xlink:href="https://github.com/xwiki-labs/cryptpad/blob/master/config/config.example.js"/>
|
||||
for a configuration example.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.cryptpad = {
|
||||
description = "Cryptpad Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
Environment = [
|
||||
"CRYPTPAD_CONFIG=${cfg.configFile}"
|
||||
"HOME=%S/cryptpad"
|
||||
];
|
||||
ExecStart = "${cfg.package}/bin/cryptpad";
|
||||
PrivateTmp = true;
|
||||
Restart = "always";
|
||||
StateDirectory = "cryptpad";
|
||||
WorkingDirectory = "%S/cryptpad";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
118
nixos/modules/services/web-apps/dex.nix
Normal file
118
nixos/modules/services/web-apps/dex.nix
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.dex;
|
||||
fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client;
|
||||
filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings;
|
||||
secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else []) (cfg.settings.staticClients or []));
|
||||
|
||||
settingsFormat = pkgs.formats.yaml {};
|
||||
configFile = settingsFormat.generate "config.yaml" filteredSettings;
|
||||
|
||||
startPreScript = pkgs.writeShellScript "dex-start-pre" (''
|
||||
'' + (concatStringsSep "\n" (builtins.map (file: ''
|
||||
${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
|
||||
'') secretFiles)));
|
||||
in
|
||||
{
|
||||
options.services.dex = {
|
||||
enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
|
||||
|
||||
settings = mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
# External url
|
||||
issuer = "http://127.0.0.1:5556/dex";
|
||||
storage = {
|
||||
type = "postgres";
|
||||
config.host = "/var/run/postgres";
|
||||
};
|
||||
web = {
|
||||
http = "127.0.0.1:5556";
|
||||
};
|
||||
enablePasswordDB = true;
|
||||
staticClients = [
|
||||
{
|
||||
id = "oidcclient";
|
||||
name = "Client";
|
||||
redirectURIs = [ "https://example.com/callback" ];
|
||||
secretFile = "/etc/dex/oidcclient"; # The content of `secretFile` will be written into to the config as `secret`.
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
The available options can be found in
|
||||
<link xlink:href="https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist">the example configuration</link>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.dex = {
|
||||
description = "dex identity provider";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
|
||||
ExecStartPre = [
|
||||
"${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
|
||||
"+${startPreScript}"
|
||||
];
|
||||
RuntimeDirectory = "dex";
|
||||
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
"-/etc/dex"
|
||||
];
|
||||
BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql";
|
||||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||||
# ProtectClock= adds DeviceAllow=char-rtc r
|
||||
DeviceAllow = "";
|
||||
DynamicUser = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
# Port needs to be exposed to the host network
|
||||
#PrivateNetwork = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
|
||||
TemporaryFileSystem = "/:ro";
|
||||
# Does not work well with the temporary root
|
||||
#UMask = "0066";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# uses attributes of the linked package
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
1093
nixos/modules/services/web-apps/discourse.nix
Normal file
1093
nixos/modules/services/web-apps/discourse.nix
Normal file
File diff suppressed because it is too large
Load diff
355
nixos/modules/services/web-apps/discourse.xml
Normal file
355
nixos/modules/services/web-apps/discourse.xml
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-discourse">
|
||||
<title>Discourse</title>
|
||||
<para>
|
||||
<link xlink:href="https://www.discourse.org/">Discourse</link> is a
|
||||
modern and open source discussion platform.
|
||||
</para>
|
||||
|
||||
<section xml:id="module-services-discourse-basic-usage">
|
||||
<title>Basic usage</title>
|
||||
<para>
|
||||
A minimal configuration using Let's Encrypt for TLS certificates looks like this:
|
||||
<programlisting>
|
||||
services.discourse = {
|
||||
<link linkend="opt-services.discourse.enable">enable</link> = true;
|
||||
<link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
|
||||
admin = {
|
||||
<link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
|
||||
<link linkend="opt-services.discourse.admin.username">username</link> = "admin";
|
||||
<link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
|
||||
<link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
|
||||
};
|
||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||
};
|
||||
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Provided a proper DNS setup, you'll be able to connect to the
|
||||
instance at <literal>discourse.example.com</literal> and log in
|
||||
using the credentials provided in
|
||||
<literal>services.discourse.admin</literal>.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-tls">
|
||||
<title>Using a regular TLS certificate</title>
|
||||
<para>
|
||||
To set up TLS using a regular certificate and key on file, use
|
||||
the <xref linkend="opt-services.discourse.sslCertificate" />
|
||||
and <xref linkend="opt-services.discourse.sslCertificateKey" />
|
||||
options:
|
||||
|
||||
<programlisting>
|
||||
services.discourse = {
|
||||
<link linkend="opt-services.discourse.enable">enable</link> = true;
|
||||
<link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
|
||||
<link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
|
||||
<link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
|
||||
admin = {
|
||||
<link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
|
||||
<link linkend="opt-services.discourse.admin.username">username</link> = "admin";
|
||||
<link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
|
||||
<link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
|
||||
};
|
||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||
};
|
||||
</programlisting>
|
||||
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-database">
|
||||
<title>Database access</title>
|
||||
<para>
|
||||
<productname>Discourse</productname> uses
|
||||
<productname>PostgreSQL</productname> to store most of its
|
||||
data. A database will automatically be enabled and a database
|
||||
and role created unless <xref
|
||||
linkend="opt-services.discourse.database.host" /> is changed from
|
||||
its default of <literal>null</literal> or <xref
|
||||
linkend="opt-services.discourse.database.createLocally" /> is set
|
||||
to <literal>false</literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
External database access can also be configured by setting
|
||||
<xref linkend="opt-services.discourse.database.host" />, <xref
|
||||
linkend="opt-services.discourse.database.username" /> and <xref
|
||||
linkend="opt-services.discourse.database.passwordFile" /> as
|
||||
appropriate. Note that you need to manually create a database
|
||||
called <literal>discourse</literal> (or the name you chose in
|
||||
<xref linkend="opt-services.discourse.database.name" />) and
|
||||
allow the configured database user full access to it.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-mail">
|
||||
<title>Email</title>
|
||||
<para>
|
||||
In addition to the basic setup, you'll want to configure an SMTP
|
||||
server <productname>Discourse</productname> can use to send user
|
||||
registration and password reset emails, among others. You can
|
||||
also optionally let <productname>Discourse</productname> receive
|
||||
email, which enables people to reply to threads and conversations
|
||||
via email.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
A basic setup which assumes you want to use your configured <link
|
||||
linkend="opt-services.discourse.hostname">hostname</link> as
|
||||
email domain can be done like this:
|
||||
|
||||
<programlisting>
|
||||
services.discourse = {
|
||||
<link linkend="opt-services.discourse.enable">enable</link> = true;
|
||||
<link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
|
||||
<link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
|
||||
<link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
|
||||
admin = {
|
||||
<link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
|
||||
<link linkend="opt-services.discourse.admin.username">username</link> = "admin";
|
||||
<link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
|
||||
<link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
|
||||
};
|
||||
mail.outgoing = {
|
||||
<link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
|
||||
<link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
|
||||
};
|
||||
<link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
|
||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||
};
|
||||
</programlisting>
|
||||
|
||||
This assumes you have set up an MX record for the address you've
|
||||
set in <link linkend="opt-services.discourse.hostname">hostname</link> and
|
||||
requires proper SPF, DKIM and DMARC configuration to be done for
|
||||
the domain you're sending from, in order for email to be reliably delivered.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
If you want to use a different domain for your outgoing email
|
||||
(for example <literal>example.com</literal> instead of
|
||||
<literal>discourse.example.com</literal>) you should set
|
||||
<xref linkend="opt-services.discourse.mail.notificationEmailAddress" /> and
|
||||
<xref linkend="opt-services.discourse.mail.contactEmailAddress" /> manually.
|
||||
</para>
|
||||
|
||||
<note>
|
||||
<para>
|
||||
Setup of TLS for incoming email is currently only configured
|
||||
automatically when a regular TLS certificate is used, i.e. when
|
||||
<xref linkend="opt-services.discourse.sslCertificate" /> and
|
||||
<xref linkend="opt-services.discourse.sslCertificateKey" /> are
|
||||
set.
|
||||
</para>
|
||||
</note>
|
||||
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-settings">
|
||||
<title>Additional settings</title>
|
||||
<para>
|
||||
Additional site settings and backend settings, for which no
|
||||
explicit <productname>NixOS</productname> options are provided,
|
||||
can be set in <xref linkend="opt-services.discourse.siteSettings" /> and
|
||||
<xref linkend="opt-services.discourse.backendSettings" /> respectively.
|
||||
</para>
|
||||
|
||||
<section xml:id="module-services-discourse-site-settings">
|
||||
<title>Site settings</title>
|
||||
<para>
|
||||
<quote>Site settings</quote> are the settings that can be
|
||||
changed through the <productname>Discourse</productname>
|
||||
UI. Their <emphasis>default</emphasis> values can be set using
|
||||
<xref linkend="opt-services.discourse.siteSettings" />.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Settings are expressed as a Nix attribute set which matches the
|
||||
structure of the configuration in
|
||||
<link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
|
||||
To find a setting's path, you only need to care about the first
|
||||
two levels; i.e. its category (e.g. <literal>login</literal>)
|
||||
and name (e.g. <literal>invite_only</literal>).
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Settings containing secret data should be set to an attribute
|
||||
set containing the attribute <literal>_secret</literal> - a
|
||||
string pointing to a file containing the value the option
|
||||
should be set to. See the example.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-backend-settings">
|
||||
<title>Backend settings</title>
|
||||
<para>
|
||||
Settings are expressed as a Nix attribute set which matches the
|
||||
structure of the configuration in
|
||||
<link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
|
||||
Empty parameters can be defined by setting them to
|
||||
<literal>null</literal>.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-discourse-settings-example">
|
||||
<title>Example</title>
|
||||
<para>
|
||||
The following example sets the title and description of the
|
||||
<productname>Discourse</productname> instance and enables
|
||||
<productname>GitHub</productname> login in the site settings,
|
||||
and changes a few request limits in the backend settings:
|
||||
<programlisting>
|
||||
services.discourse = {
|
||||
<link linkend="opt-services.discourse.enable">enable</link> = true;
|
||||
<link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
|
||||
<link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
|
||||
<link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
|
||||
admin = {
|
||||
<link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
|
||||
<link linkend="opt-services.discourse.admin.username">username</link> = "admin";
|
||||
<link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
|
||||
<link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
|
||||
};
|
||||
mail.outgoing = {
|
||||
<link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
|
||||
<link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
|
||||
};
|
||||
<link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
|
||||
<link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
|
||||
required = {
|
||||
title = "My Cats";
|
||||
site_description = "Discuss My Cats (and be nice plz)";
|
||||
};
|
||||
login = {
|
||||
enable_github_logins = true;
|
||||
github_client_id = "a2f6dfe838cb3206ce20";
|
||||
github_client_secret._secret = /run/keys/discourse_github_client_secret;
|
||||
};
|
||||
};
|
||||
<link linkend="opt-services.discourse.backendSettings">backendSettings</link> = {
|
||||
max_reqs_per_ip_per_minute = 300;
|
||||
max_reqs_per_ip_per_10_seconds = 60;
|
||||
max_asset_reqs_per_ip_per_10_seconds = 250;
|
||||
max_reqs_per_ip_mode = "warn+block";
|
||||
};
|
||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||
};
|
||||
</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
In the resulting site settings file, the
|
||||
<literal>login.github_client_secret</literal> key will be set
|
||||
to the contents of the
|
||||
<filename>/run/keys/discourse_github_client_secret</filename>
|
||||
file.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
<section xml:id="module-services-discourse-plugins">
|
||||
<title>Plugins</title>
|
||||
<para>
|
||||
You can install <productname>Discourse</productname> plugins
|
||||
using the <xref linkend="opt-services.discourse.plugins" />
|
||||
option. Pre-packaged plugins are provided in
|
||||
<literal><your_discourse_package_here>.plugins</literal>. If
|
||||
you want the full suite of plugins provided through
|
||||
<literal>nixpkgs</literal>, you can also set the <xref
|
||||
linkend="opt-services.discourse.package" /> option to
|
||||
<literal>pkgs.discourseAllPlugins</literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Plugins can be built with the
|
||||
<literal><your_discourse_package_here>.mkDiscoursePlugin</literal>
|
||||
function. Normally, it should suffice to provide a
|
||||
<literal>name</literal> and <literal>src</literal> attribute. If
|
||||
the plugin has Ruby dependencies, however, they need to be
|
||||
packaged in accordance with the <link
|
||||
xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
|
||||
with Ruby</link> section of the Nixpkgs manual and the
|
||||
appropriate gem options set in <literal>bundlerEnvArgs</literal>
|
||||
(normally <literal>gemdir</literal> is sufficient). A plugin's
|
||||
Ruby dependencies are listed in its
|
||||
<filename>plugin.rb</filename> file as function calls to
|
||||
<literal>gem</literal>. To construct the corresponding
|
||||
<filename>Gemfile</filename> manually, run <command>bundle
|
||||
init</command>, then add the <literal>gem</literal> lines to it
|
||||
verbatim.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Much of the packaging can be done automatically by the
|
||||
<filename>nixpkgs/pkgs/servers/web-apps/discourse/update.py</filename>
|
||||
script - just add the plugin to the <literal>plugins</literal>
|
||||
list in the <function>update_plugins</function> function and run
|
||||
the script:
|
||||
<programlisting language="bash">
|
||||
./update.py update-plugins
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Some plugins provide <link
|
||||
linkend="module-services-discourse-site-settings">site
|
||||
settings</link>. Their defaults can be configured using <xref
|
||||
linkend="opt-services.discourse.siteSettings" />, just like
|
||||
regular site settings. To find the names of these settings, look
|
||||
in the <literal>config/settings.yml</literal> file of the plugin
|
||||
repo.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
For example, to add the <link
|
||||
xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
|
||||
and <link
|
||||
xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
|
||||
plugins, and disable <literal>discourse-spoiler-alert</literal>
|
||||
by default:
|
||||
|
||||
<programlisting>
|
||||
services.discourse = {
|
||||
<link linkend="opt-services.discourse.enable">enable</link> = true;
|
||||
<link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
|
||||
<link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
|
||||
<link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
|
||||
admin = {
|
||||
<link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
|
||||
<link linkend="opt-services.discourse.admin.username">username</link> = "admin";
|
||||
<link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
|
||||
<link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
|
||||
};
|
||||
mail.outgoing = {
|
||||
<link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
|
||||
<link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
|
||||
<link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
|
||||
};
|
||||
<link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
|
||||
<link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
|
||||
discourse-spoiler-alert
|
||||
discourse-solved
|
||||
];
|
||||
<link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
|
||||
plugins = {
|
||||
spoiler_enabled = false;
|
||||
};
|
||||
};
|
||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||
};
|
||||
</programlisting>
|
||||
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
150
nixos/modules/services/web-apps/documize.nix
Normal file
150
nixos/modules/services/web-apps/documize.nix
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{ pkgs, lib, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.documize;
|
||||
|
||||
mkParams = optional: concatMapStrings (name: let
|
||||
predicate = optional -> cfg.${name} != null;
|
||||
template = " -${name} '${toString cfg.${name}}'";
|
||||
in optionalString predicate template);
|
||||
|
||||
in {
|
||||
options.services.documize = {
|
||||
enable = mkEnableOption "Documize Wiki";
|
||||
|
||||
stateDirectoryName = mkOption {
|
||||
type = types.str;
|
||||
default = "documize";
|
||||
description = ''
|
||||
The name of the directory below <filename>/var/lib/private</filename>
|
||||
where documize runs in and stores, for example, backups.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.documize-community;
|
||||
defaultText = literalExpression "pkgs.documize-community";
|
||||
description = ''
|
||||
Which package to use for documize.
|
||||
'';
|
||||
};
|
||||
|
||||
salt = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "3edIYV6c8B28b19fh";
|
||||
description = ''
|
||||
The salt string used to encode JWT tokens, if not set a random value will be generated.
|
||||
'';
|
||||
};
|
||||
|
||||
cert = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The <filename>cert.pem</filename> file used for https.
|
||||
'';
|
||||
};
|
||||
|
||||
key = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The <filename>key.pem</filename> file used for https.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5001;
|
||||
description = ''
|
||||
The http/https port number.
|
||||
'';
|
||||
};
|
||||
|
||||
forcesslport = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = ''
|
||||
Redirect given http port number to TLS.
|
||||
'';
|
||||
};
|
||||
|
||||
offline = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Set <literal>true</literal> for offline mode.
|
||||
'';
|
||||
apply = v: if true == v then 1 else 0;
|
||||
};
|
||||
|
||||
dbtype = mkOption {
|
||||
type = types.enum [ "mysql" "percona" "mariadb" "postgresql" "sqlserver" ];
|
||||
default = "postgresql";
|
||||
description = ''
|
||||
Specify the database provider:
|
||||
<simplelist type='inline'>
|
||||
<member><literal>mysql</literal></member>
|
||||
<member><literal>percona</literal></member>
|
||||
<member><literal>mariadb</literal></member>
|
||||
<member><literal>postgresql</literal></member>
|
||||
<member><literal>sqlserver</literal></member>
|
||||
</simplelist>
|
||||
'';
|
||||
};
|
||||
|
||||
db = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Database specific connection string for example:
|
||||
<itemizedlist>
|
||||
<listitem><para>MySQL/Percona/MariaDB:
|
||||
<literal>user:password@tcp(host:3306)/documize</literal>
|
||||
</para></listitem>
|
||||
<listitem><para>MySQLv8+:
|
||||
<literal>user:password@tcp(host:3306)/documize?allowNativePasswords=true</literal>
|
||||
</para></listitem>
|
||||
<listitem><para>PostgreSQL:
|
||||
<literal>host=localhost port=5432 dbname=documize user=admin password=secret sslmode=disable</literal>
|
||||
</para></listitem>
|
||||
<listitem><para>MSSQL:
|
||||
<literal>sqlserver://username:password@localhost:1433?database=Documize</literal> or
|
||||
<literal>sqlserver://sa@localhost/SQLExpress?database=Documize</literal>
|
||||
</para></listitem>
|
||||
</itemizedlist>
|
||||
'';
|
||||
};
|
||||
|
||||
location = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
reserved
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.documize-server = {
|
||||
description = "Documize Wiki";
|
||||
documentation = [ "https://documize.com/" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = concatStringsSep " " [
|
||||
"${cfg.package}/bin/documize"
|
||||
(mkParams false [ "db" "dbtype" "port" ])
|
||||
(mkParams true [ "offline" "location" "forcesslport" "key" "cert" "salt" ])
|
||||
];
|
||||
Restart = "always";
|
||||
DynamicUser = "yes";
|
||||
StateDirectory = cfg.stateDirectoryName;
|
||||
WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
439
nixos/modules/services/web-apps/dokuwiki.nix
Normal file
439
nixos/modules/services/web-apps/dokuwiki.nix
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.dokuwiki;
|
||||
eachSite = cfg.sites;
|
||||
user = "dokuwiki";
|
||||
webserver = config.services.${cfg.webserver};
|
||||
stateDir = hostName: "/var/lib/dokuwiki/${hostName}/data";
|
||||
|
||||
dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
|
||||
# acl.auth.php
|
||||
# <?php exit()?>
|
||||
#
|
||||
# Access Control Lists
|
||||
#
|
||||
${toString cfg.acl}
|
||||
'';
|
||||
|
||||
dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" ''
|
||||
<?php
|
||||
$conf['savedir'] = '${cfg.stateDir}';
|
||||
$conf['superuser'] = '${toString cfg.superUser}';
|
||||
$conf['useacl'] = '${toString cfg.aclUse}';
|
||||
$conf['disableactions'] = '${cfg.disableActions}';
|
||||
${toString cfg.extraConfig}
|
||||
'';
|
||||
|
||||
dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
|
||||
<?php
|
||||
${cfg.pluginsConfig}
|
||||
'';
|
||||
|
||||
|
||||
pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
|
||||
pname = "dokuwiki-${hostName}";
|
||||
version = src.version;
|
||||
src = cfg.package;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r * $out/
|
||||
|
||||
# symlink the dokuwiki config
|
||||
ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
|
||||
|
||||
# symlink plugins config
|
||||
ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
|
||||
|
||||
# symlink acl
|
||||
ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
|
||||
|
||||
# symlink additional plugin(s) and templates(s)
|
||||
${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
|
||||
${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
|
||||
'';
|
||||
};
|
||||
|
||||
siteOpts = { config, lib, name, ... }:
|
||||
{
|
||||
options = {
|
||||
enable = mkEnableOption "DokuWiki web application.";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.dokuwiki;
|
||||
defaultText = literalExpression "pkgs.dokuwiki";
|
||||
description = "Which DokuWiki package to use.";
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/dokuwiki/${name}/data";
|
||||
description = "Location of the DokuWiki state directory.";
|
||||
};
|
||||
|
||||
acl = mkOption {
|
||||
type = types.nullOr types.lines;
|
||||
default = null;
|
||||
example = "* @ALL 8";
|
||||
description = ''
|
||||
Access Control Lists: see <link xlink:href="https://www.dokuwiki.org/acl"/>
|
||||
Mutually exclusive with services.dokuwiki.aclFile
|
||||
Set this to a value other than null to take precedence over aclFile option.
|
||||
|
||||
Warning: Consider using aclFile instead if you do not
|
||||
want to store the ACL in the world-readable Nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
aclFile = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
|
||||
description = ''
|
||||
Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
|
||||
Mutually exclusive with services.dokuwiki.acl which is preferred.
|
||||
Consult documentation <link xlink:href="https://www.dokuwiki.org/acl"/> for further instructions.
|
||||
Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/acl.auth.php.dist"/>
|
||||
'';
|
||||
example = "/var/lib/dokuwiki/${name}/acl.auth.php";
|
||||
};
|
||||
|
||||
aclUse = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Necessary for users to log in into the system.
|
||||
Also limits anonymous users. When disabled,
|
||||
everyone is able to create and edit content.
|
||||
'';
|
||||
};
|
||||
|
||||
pluginsConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = ''
|
||||
$plugins['authad'] = 0;
|
||||
$plugins['authldap'] = 0;
|
||||
$plugins['authmysql'] = 0;
|
||||
$plugins['authpgsql'] = 0;
|
||||
'';
|
||||
description = ''
|
||||
List of the dokuwiki (un)loaded plugins.
|
||||
'';
|
||||
};
|
||||
|
||||
superUser = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "@admin";
|
||||
description = ''
|
||||
You can set either a username, a list of usernames (“admin1,admin2”),
|
||||
or the name of a group by prepending an @ char to the groupname
|
||||
Consult documentation <link xlink:href="https://www.dokuwiki.org/config:superuser"/> for further instructions.
|
||||
'';
|
||||
};
|
||||
|
||||
usersFile = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
|
||||
description = ''
|
||||
Location of the dokuwiki users file. List of users. Format:
|
||||
login:passwordhash:Real Name:email:groups,comma,separated
|
||||
Create passwordHash easily by using:$ mkpasswd -5 password `pwgen 8 1`
|
||||
Example: <link xlink:href="https://github.com/splitbrain/dokuwiki/blob/master/conf/users.auth.php.dist"/>
|
||||
'';
|
||||
example = "/var/lib/dokuwiki/${name}/users.auth.php";
|
||||
};
|
||||
|
||||
disableActions = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "";
|
||||
example = "search,register";
|
||||
description = ''
|
||||
Disable individual action modes. Refer to
|
||||
<link xlink:href="https://www.dokuwiki.org/config:action_modes"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
description = ''
|
||||
List of path(s) to respective plugin(s) which are copied from the 'plugin' directory.
|
||||
<note><para>These plugins need to be packaged before use, see example.</para></note>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
let
|
||||
# Let's package the icalevents plugin
|
||||
plugin-icalevents = pkgs.stdenv.mkDerivation {
|
||||
name = "icalevents";
|
||||
# Download the plugin from the dokuwiki site
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip";
|
||||
sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
|
||||
};
|
||||
sourceRoot = ".";
|
||||
# We need unzip to build this package
|
||||
buildInputs = [ pkgs.unzip ];
|
||||
# Installing simply means copying all files to the output directory
|
||||
installPhase = "mkdir -p $out; cp -R * $out/";
|
||||
};
|
||||
# And then pass this theme to the plugin list like this:
|
||||
in [ plugin-icalevents ]
|
||||
'';
|
||||
};
|
||||
|
||||
templates = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
description = ''
|
||||
List of path(s) to respective template(s) which are copied from the 'tpl' directory.
|
||||
<note><para>These templates need to be packaged before use, see example.</para></note>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
let
|
||||
# Let's package the bootstrap3 theme
|
||||
template-bootstrap3 = pkgs.stdenv.mkDerivation {
|
||||
name = "bootstrap3";
|
||||
# Download the theme from the dokuwiki site
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip";
|
||||
sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
|
||||
};
|
||||
# We need unzip to build this package
|
||||
buildInputs = [ pkgs.unzip ];
|
||||
# Installing simply means copying all files to the output directory
|
||||
installPhase = "mkdir -p $out; cp -R * $out/";
|
||||
};
|
||||
# And then pass this theme to the template list like this:
|
||||
in [ template-bootstrap3 ]
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the DokuWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.nullOr types.lines;
|
||||
default = null;
|
||||
example = ''
|
||||
$conf['title'] = 'My Wiki';
|
||||
$conf['userewrite'] = 1;
|
||||
'';
|
||||
description = ''
|
||||
DokuWiki configuration. Refer to
|
||||
<link xlink:href="https://www.dokuwiki.org/config"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
in
|
||||
{
|
||||
# interface
|
||||
options = {
|
||||
services.dokuwiki = {
|
||||
|
||||
sites = mkOption {
|
||||
type = types.attrsOf (types.submodule siteOpts);
|
||||
default = {};
|
||||
description = "Specification of one or more DokuWiki sites to serve";
|
||||
};
|
||||
|
||||
webserver = mkOption {
|
||||
type = types.enum [ "nginx" "caddy" ];
|
||||
default = "nginx";
|
||||
description = ''
|
||||
Whether to use nginx or caddy for virtual host management.
|
||||
|
||||
Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
|
||||
|
||||
Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
config = mkIf (eachSite != {}) (mkMerge [{
|
||||
|
||||
assertions = flatten (mapAttrsToList (hostName: cfg:
|
||||
[{
|
||||
assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
|
||||
message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if aclUse true";
|
||||
}
|
||||
{
|
||||
assertion = cfg.usersFile != null -> cfg.aclUse != false;
|
||||
message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null";
|
||||
}
|
||||
]) eachSite);
|
||||
|
||||
services.phpfpm.pools = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "dokuwiki-${hostName}" {
|
||||
inherit user;
|
||||
group = webserver.group;
|
||||
|
||||
# Not yet compatible with php 8 https://www.dokuwiki.org/requirements
|
||||
# https://github.com/splitbrain/dokuwiki/issues/3545
|
||||
phpPackage = pkgs.php74;
|
||||
phpEnv = {
|
||||
DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
|
||||
DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
|
||||
} // optionalAttrs (cfg.usersFile != null) {
|
||||
DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
|
||||
} //optionalAttrs (cfg.aclUse) {
|
||||
DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
|
||||
};
|
||||
|
||||
settings = {
|
||||
"listen.owner" = webserver.user;
|
||||
"listen.group" = webserver.group;
|
||||
} // cfg.poolConfig;
|
||||
}
|
||||
)) eachSite;
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
|
||||
"d ${stateDir hostName}/attic 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/cache 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/index 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/locks 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/media 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/media_attic 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/media_meta 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/meta 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/pages 0750 ${user} ${webserver.group} - -"
|
||||
"d ${stateDir hostName}/tmp 0750 ${user} ${webserver.group} - -"
|
||||
] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist"
|
||||
++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist"
|
||||
) eachSite);
|
||||
|
||||
users.users.${user} = {
|
||||
group = webserver.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
}
|
||||
|
||||
(mkIf (cfg.webserver == "nginx") {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = mapAttrs (hostName: cfg: {
|
||||
serverName = mkDefault hostName;
|
||||
root = "${pkg hostName cfg}/share/dokuwiki";
|
||||
|
||||
locations = {
|
||||
"~ /(conf/|bin/|inc/|install.php)" = {
|
||||
extraConfig = "deny all;";
|
||||
};
|
||||
|
||||
"~ ^/data/" = {
|
||||
root = "${stateDir hostName}";
|
||||
extraConfig = "internal;";
|
||||
};
|
||||
|
||||
"~ ^/lib.*\.(js|css|gif|png|ico|jpg|jpeg)$" = {
|
||||
extraConfig = "expires 365d;";
|
||||
};
|
||||
|
||||
"/" = {
|
||||
priority = 1;
|
||||
index = "doku.php";
|
||||
extraConfig = ''try_files $uri $uri/ @dokuwiki;'';
|
||||
};
|
||||
|
||||
"@dokuwiki" = {
|
||||
extraConfig = ''
|
||||
# rewrites "doku.php/" out of the URLs if you set the userwrite setting to .htaccess in dokuwiki config page
|
||||
rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;
|
||||
rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;
|
||||
rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;
|
||||
rewrite ^/(.*) /doku.php?id=$1&$args last;
|
||||
'';
|
||||
};
|
||||
|
||||
"~ \\.php$" = {
|
||||
extraConfig = ''
|
||||
try_files $uri $uri/ /doku.php;
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools."dokuwiki-${hostName}".socket};
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
}) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
(mkIf (cfg.webserver == "caddy") {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "http://${hostName}" {
|
||||
extraConfig = ''
|
||||
root * ${pkg hostName cfg}/share/dokuwiki
|
||||
file_server
|
||||
|
||||
encode zstd gzip
|
||||
php_fastcgi unix/${config.services.phpfpm.pools."dokuwiki-${hostName}".socket}
|
||||
|
||||
@restrict_files {
|
||||
path /data/* /conf/* /bin/* /inc/* /vendor/* /install.php
|
||||
}
|
||||
|
||||
respond @restrict_files 404
|
||||
|
||||
@allow_media {
|
||||
path_regexp path ^/_media/(.*)$
|
||||
}
|
||||
rewrite @allow_media /lib/exe/fetch.php?media=/{http.regexp.path.1}
|
||||
|
||||
@allow_detail {
|
||||
path /_detail*
|
||||
}
|
||||
rewrite @allow_detail /lib/exe/detail.php?media={path}
|
||||
|
||||
@allow_export {
|
||||
path /_export*
|
||||
path_regexp export /([^/]+)/(.*)
|
||||
}
|
||||
rewrite @allow_export /doku.php?do=export_{http.regexp.export.1}&id={http.regexp.export.2}
|
||||
|
||||
try_files {path} {path}/ /doku.php?id={path}&{query}
|
||||
'';
|
||||
}
|
||||
)) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
]);
|
||||
|
||||
meta.maintainers = with maintainers; [
|
||||
_1000101
|
||||
onny
|
||||
dandellion
|
||||
];
|
||||
}
|
||||
186
nixos/modules/services/web-apps/engelsystem.nix
Normal file
186
nixos/modules/services/web-apps/engelsystem.nix
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
{ config, lib, pkgs, utils, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkDefault mkEnableOption mkIf mkOption types literalExpression;
|
||||
cfg = config.services.engelsystem;
|
||||
in {
|
||||
options = {
|
||||
services.engelsystem = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether to enable engelsystem, an online tool for coordinating volunteers
|
||||
and shifts on large events.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
example = "engelsystem.example.com";
|
||||
description = "Domain to serve on.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
description = "Engelsystem package used for the service.";
|
||||
default = pkgs.engelsystem;
|
||||
defaultText = literalExpression "pkgs.engelsystem";
|
||||
};
|
||||
|
||||
createDatabase = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to create a local database automatically.
|
||||
This will override every database setting in <option>services.engelsystem.config</option>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
services.engelsystem.config = mkOption {
|
||||
type = types.attrs;
|
||||
default = {
|
||||
database = {
|
||||
host = "localhost";
|
||||
database = "engelsystem";
|
||||
username = "engelsystem";
|
||||
};
|
||||
};
|
||||
example = {
|
||||
maintenance = false;
|
||||
database = {
|
||||
host = "database.example.com";
|
||||
database = "engelsystem";
|
||||
username = "engelsystem";
|
||||
password._secret = "/var/keys/engelsystem/database";
|
||||
};
|
||||
email = {
|
||||
driver = "smtp";
|
||||
host = "smtp.example.com";
|
||||
port = 587;
|
||||
from.address = "engelsystem@example.com";
|
||||
from.name = "example engelsystem";
|
||||
encryption = "tls";
|
||||
username = "engelsystem@example.com";
|
||||
password._secret = "/var/keys/engelsystem/mail";
|
||||
};
|
||||
autoarrive = true;
|
||||
min_password_length = 6;
|
||||
default_locale = "de_DE";
|
||||
};
|
||||
description = ''
|
||||
Options to be added to config.php, as a nix attribute set. Options containing secret data
|
||||
should be set to an attribute set containing the attribute _secret - a string pointing to a
|
||||
file containing the value the option should be set to. See the example to get a better
|
||||
picture of this: in the resulting config.php file, the email.password key will be set to
|
||||
the contents of the /var/keys/engelsystem/mail file.
|
||||
|
||||
See https://engelsystem.de/doc/admin/configuration/ for available options.
|
||||
|
||||
Note that the admin user login credentials cannot be set here - they always default to
|
||||
admin:asdfasdf. Log in and change them immediately.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# create database
|
||||
services.mysql = mkIf cfg.createDatabase {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureUsers = [{
|
||||
name = "engelsystem";
|
||||
ensurePermissions = { "engelsystem.*" = "ALL PRIVILEGES"; };
|
||||
}];
|
||||
ensureDatabases = [ "engelsystem" ];
|
||||
};
|
||||
|
||||
environment.etc."engelsystem/config.php".source =
|
||||
pkgs.writeText "config.php" ''
|
||||
<?php
|
||||
return json_decode(file_get_contents("/var/lib/engelsystem/config.json"), true);
|
||||
'';
|
||||
|
||||
services.phpfpm.pools.engelsystem = {
|
||||
user = "engelsystem";
|
||||
settings = {
|
||||
"listen.owner" = config.services.nginx.user;
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.max_requests" = 500;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 5;
|
||||
"php_admin_value[error_log]" = "stderr";
|
||||
"php_admin_flag[log_errors]" = true;
|
||||
"catch_workers_output" = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.domain}".locations = {
|
||||
"/" = {
|
||||
root = "${cfg.package}/share/engelsystem/public";
|
||||
extraConfig = ''
|
||||
index index.php;
|
||||
try_files $uri $uri/ /index.php?$args;
|
||||
autoindex off;
|
||||
'';
|
||||
};
|
||||
"~ \\.php$" = {
|
||||
root = "${cfg.package}/share/engelsystem/public";
|
||||
extraConfig = ''
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.engelsystem.socket};
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."engelsystem-init" = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = { Type = "oneshot"; };
|
||||
script =
|
||||
let
|
||||
genConfigScript = pkgs.writeScript "engelsystem-gen-config.sh"
|
||||
(utils.genJqSecretsReplacementSnippet cfg.config "config.json");
|
||||
in ''
|
||||
umask 077
|
||||
mkdir -p /var/lib/engelsystem/storage/app
|
||||
mkdir -p /var/lib/engelsystem/storage/cache/views
|
||||
cd /var/lib/engelsystem
|
||||
${genConfigScript}
|
||||
chmod 400 config.json
|
||||
chown -R engelsystem .
|
||||
'';
|
||||
};
|
||||
systemd.services."engelsystem-migrate" = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "engelsystem";
|
||||
Group = "engelsystem";
|
||||
};
|
||||
script = ''
|
||||
${cfg.package}/bin/migrate
|
||||
'';
|
||||
after = [ "engelsystem-init.service" "mysql.service" ];
|
||||
};
|
||||
systemd.services."phpfpm-engelsystem".after =
|
||||
[ "engelsystem-migrate.service" ];
|
||||
|
||||
users.users.engelsystem = {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = "/var/lib/engelsystem/storage";
|
||||
group = "engelsystem";
|
||||
};
|
||||
users.groups.engelsystem = { };
|
||||
};
|
||||
}
|
||||
62
nixos/modules/services/web-apps/ethercalc.nix
Normal file
62
nixos/modules/services/web-apps/ethercalc.nix
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.ethercalc;
|
||||
in {
|
||||
options = {
|
||||
services.ethercalc = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = ''
|
||||
ethercalc, an online collaborative spreadsheet server.
|
||||
|
||||
Persistent state will be maintained under
|
||||
<filename>/var/lib/ethercalc</filename>. Upstream supports using a
|
||||
redis server for storage and recommends the redis backend for
|
||||
intensive use; however, the Nix module doesn't currently support
|
||||
redis.
|
||||
|
||||
Note that while ethercalc is a good and robust project with an active
|
||||
issue tracker, there haven't been new commits since the end of 2020.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.ethercalc;
|
||||
defaultText = literalExpression "pkgs.ethercalc";
|
||||
type = types.package;
|
||||
description = "Ethercalc package to use.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "0.0.0.0";
|
||||
description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8000;
|
||||
description = "Port to bind to.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.ethercalc = {
|
||||
description = "Ethercalc service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
ExecStart = "${cfg.package}/bin/ethercalc --host ${cfg.host} --port ${toString cfg.port}";
|
||||
Restart = "always";
|
||||
StateDirectory = "ethercalc";
|
||||
WorkingDirectory = "/var/lib/ethercalc";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
66
nixos/modules/services/web-apps/fluidd.nix
Normal file
66
nixos/modules/services/web-apps/fluidd.nix
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.fluidd;
|
||||
moonraker = config.services.moonraker;
|
||||
in
|
||||
{
|
||||
options.services.fluidd = {
|
||||
enable = mkEnableOption "Fluidd, a Klipper web interface for managing your 3d printer";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
description = "Fluidd package to be used in the module";
|
||||
default = pkgs.fluidd;
|
||||
defaultText = literalExpression "pkgs.fluidd";
|
||||
};
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Hostname to serve fluidd on";
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
type = types.submodule
|
||||
(import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
serverAliases = [ "fluidd.''${config.networking.domain}" ];
|
||||
}
|
||||
'';
|
||||
description = "Extra configuration for the nginx virtual host of fluidd.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
upstreams.fluidd-apiserver.servers."${moonraker.address}:${toString moonraker.port}" = { };
|
||||
virtualHosts."${cfg.hostName}" = mkMerge [
|
||||
cfg.nginx
|
||||
{
|
||||
root = mkForce "${cfg.package}/share/fluidd/htdocs";
|
||||
locations = {
|
||||
"/" = {
|
||||
index = "index.html";
|
||||
tryFiles = "$uri $uri/ /index.html";
|
||||
};
|
||||
"/index.html".extraConfig = ''
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
'';
|
||||
"/websocket" = {
|
||||
proxyWebsockets = true;
|
||||
proxyPass = "http://fluidd-apiserver/websocket";
|
||||
};
|
||||
"~ ^/(printer|api|access|machine|server)/" = {
|
||||
proxyWebsockets = true;
|
||||
proxyPass = "http://fluidd-apiserver$request_uri";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
214
nixos/modules/services/web-apps/galene.nix
Normal file
214
nixos/modules/services/web-apps/galene.nix
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
{ config, lib, options, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.galene;
|
||||
opt = options.services.galene;
|
||||
defaultstateDir = "/var/lib/galene";
|
||||
defaultrecordingsDir = "${cfg.stateDir}/recordings";
|
||||
defaultgroupsDir = "${cfg.stateDir}/groups";
|
||||
defaultdataDir = "${cfg.stateDir}/data";
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.galene = {
|
||||
enable = mkEnableOption "Galene Service.";
|
||||
|
||||
stateDir = mkOption {
|
||||
default = defaultstateDir;
|
||||
type = types.str;
|
||||
description = ''
|
||||
The directory where Galene stores its internal state. If left as the default
|
||||
value this directory will automatically be created before the Galene server
|
||||
starts, otherwise the sysadmin is responsible for ensuring the directory
|
||||
exists with appropriate ownership and permissions.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "galene";
|
||||
description = "User account under which galene runs.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "galene";
|
||||
description = "Group under which galene runs.";
|
||||
};
|
||||
|
||||
insecure = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether Galene should listen in http or in https. If left as the default
|
||||
value (false), Galene needs to be fed a private key and a certificate.
|
||||
'';
|
||||
};
|
||||
|
||||
certFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "/path/to/your/cert.pem";
|
||||
description = ''
|
||||
Path to the server's certificate. The file is copied at runtime to
|
||||
Galene's data directory where it needs to reside.
|
||||
'';
|
||||
};
|
||||
|
||||
keyFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "/path/to/your/key.pem";
|
||||
description = ''
|
||||
Path to the server's private key. The file is copied at runtime to
|
||||
Galene's data directory where it needs to reside.
|
||||
'';
|
||||
};
|
||||
|
||||
httpAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "HTTP listen address for galene.";
|
||||
};
|
||||
|
||||
httpPort = mkOption {
|
||||
type = types.port;
|
||||
default = 8443;
|
||||
description = "HTTP listen port.";
|
||||
};
|
||||
|
||||
staticDir = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.package.static}/static";
|
||||
defaultText = literalExpression ''"''${package.static}/static"'';
|
||||
example = "/var/lib/galene/static";
|
||||
description = "Web server directory.";
|
||||
};
|
||||
|
||||
recordingsDir = mkOption {
|
||||
type = types.str;
|
||||
default = defaultrecordingsDir;
|
||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/recordings"'';
|
||||
example = "/var/lib/galene/recordings";
|
||||
description = "Recordings directory.";
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = defaultdataDir;
|
||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/data"'';
|
||||
example = "/var/lib/galene/data";
|
||||
description = "Data directory.";
|
||||
};
|
||||
|
||||
groupsDir = mkOption {
|
||||
type = types.str;
|
||||
default = defaultgroupsDir;
|
||||
defaultText = literalExpression ''"''${config.${opt.stateDir}}/groups"'';
|
||||
example = "/var/lib/galene/groups";
|
||||
description = "Web server directory.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.galene;
|
||||
defaultText = literalExpression "pkgs.galene";
|
||||
type = types.package;
|
||||
description = ''
|
||||
Package for running Galene.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.insecure || (cfg.certFile != null && cfg.keyFile != null);
|
||||
message = ''
|
||||
Galene needs both certFile and keyFile defined for encryption, or
|
||||
the insecure flag.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.galene = {
|
||||
description = "galene";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
preStart = ''
|
||||
${optionalString (cfg.insecure != true) ''
|
||||
install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.certFile} ${cfg.dataDir}/cert.pem
|
||||
install -m 700 -o '${cfg.user}' -g '${cfg.group}' ${cfg.keyFile} ${cfg.dataDir}/key.pem
|
||||
''}
|
||||
'';
|
||||
|
||||
serviceConfig = mkMerge [
|
||||
{
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
ExecStart = ''${cfg.package}/bin/galene \
|
||||
${optionalString (cfg.insecure) "-insecure"} \
|
||||
-data ${cfg.dataDir} \
|
||||
-groups ${cfg.groupsDir} \
|
||||
-recordings ${cfg.recordingsDir} \
|
||||
-static ${cfg.staticDir}'';
|
||||
Restart = "always";
|
||||
# Upstream Requirements
|
||||
LimitNOFILE = 65536;
|
||||
StateDirectory = [ ] ++
|
||||
optional (cfg.stateDir == defaultstateDir) "galene" ++
|
||||
optional (cfg.dataDir == defaultdataDir) "galene/data" ++
|
||||
optional (cfg.groupsDir == defaultgroupsDir) "galene/groups" ++
|
||||
optional (cfg.recordingsDir == defaultrecordingsDir) "galene/recordings";
|
||||
|
||||
# Hardening
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DeviceAllow = [ "" ];
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
ReadWritePaths = cfg.recordingsDir;
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
UMask = "0077";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
users.users = mkIf (cfg.user == "galene")
|
||||
{
|
||||
galene = {
|
||||
description = "galene Service";
|
||||
group = cfg.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = mkIf (cfg.group == "galene") {
|
||||
galene = { };
|
||||
};
|
||||
};
|
||||
meta.maintainers = with lib.maintainers; [ rgrunbla ];
|
||||
}
|
||||
242
nixos/modules/services/web-apps/gerrit.nix
Normal file
242
nixos/modules/services/web-apps/gerrit.nix
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.gerrit;
|
||||
|
||||
# NixOS option type for git-like configs
|
||||
gitIniType = with types;
|
||||
let
|
||||
primitiveType = either str (either bool int);
|
||||
multipleType = either primitiveType (listOf primitiveType);
|
||||
sectionType = lazyAttrsOf multipleType;
|
||||
supersectionType = lazyAttrsOf (either multipleType sectionType);
|
||||
in lazyAttrsOf supersectionType;
|
||||
|
||||
gerritConfig = pkgs.writeText "gerrit.conf" (
|
||||
lib.generators.toGitINI cfg.settings
|
||||
);
|
||||
|
||||
replicationConfig = pkgs.writeText "replication.conf" (
|
||||
lib.generators.toGitINI cfg.replicationSettings
|
||||
);
|
||||
|
||||
# Wrap the gerrit java with all the java options so it can be called
|
||||
# like a normal CLI app
|
||||
gerrit-cli = pkgs.writeShellScriptBin "gerrit" ''
|
||||
set -euo pipefail
|
||||
jvmOpts=(
|
||||
${lib.escapeShellArgs cfg.jvmOpts}
|
||||
-Xmx${cfg.jvmHeapLimit}
|
||||
)
|
||||
exec ${cfg.jvmPackage}/bin/java \
|
||||
"''${jvmOpts[@]}" \
|
||||
-jar ${cfg.package}/webapps/${cfg.package.name}.war \
|
||||
"$@"
|
||||
'';
|
||||
|
||||
gerrit-plugins = pkgs.runCommand
|
||||
"gerrit-plugins"
|
||||
{
|
||||
buildInputs = [ gerrit-cli ];
|
||||
}
|
||||
''
|
||||
shopt -s nullglob
|
||||
mkdir $out
|
||||
|
||||
for name in ${toString cfg.builtinPlugins}; do
|
||||
echo "Installing builtin plugin $name.jar"
|
||||
gerrit cat plugins/$name.jar > $out/$name.jar
|
||||
done
|
||||
|
||||
for file in ${toString cfg.plugins}; do
|
||||
name=$(echo "$file" | cut -d - -f 2-)
|
||||
echo "Installing plugin $name"
|
||||
ln -sf "$file" $out/$name
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.gerrit = {
|
||||
enable = mkEnableOption "Gerrit service";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.gerrit;
|
||||
defaultText = literalExpression "pkgs.gerrit";
|
||||
description = "Gerrit package to use";
|
||||
};
|
||||
|
||||
jvmPackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jre_headless;
|
||||
defaultText = literalExpression "pkgs.jre_headless";
|
||||
description = "Java Runtime Environment package to use";
|
||||
};
|
||||
|
||||
jvmOpts = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [
|
||||
"-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"
|
||||
"-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance"
|
||||
];
|
||||
description = "A list of JVM options to start gerrit with.";
|
||||
};
|
||||
|
||||
jvmHeapLimit = mkOption {
|
||||
type = types.str;
|
||||
default = "1024m";
|
||||
description = ''
|
||||
How much memory to allocate to the JVM heap
|
||||
'';
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "[::]:8080";
|
||||
description = ''
|
||||
<literal>hostname:port</literal> to listen for HTTP traffic.
|
||||
|
||||
This is bound using the systemd socket activation.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = gitIniType;
|
||||
default = {};
|
||||
description = ''
|
||||
Gerrit configuration. This will be generated to the
|
||||
<literal>etc/gerrit.config</literal> file.
|
||||
'';
|
||||
};
|
||||
|
||||
replicationSettings = mkOption {
|
||||
type = gitIniType;
|
||||
default = {};
|
||||
description = ''
|
||||
Replication configuration. This will be generated to the
|
||||
<literal>etc/replication.config</literal> file.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
description = ''
|
||||
List of plugins to add to Gerrit. Each derivation is a jar file
|
||||
itself where the name of the derivation is the name of plugin.
|
||||
'';
|
||||
};
|
||||
|
||||
builtinPlugins = mkOption {
|
||||
type = types.listOf (types.enum cfg.package.passthru.plugins);
|
||||
default = [];
|
||||
description = ''
|
||||
List of builtins plugins to install. Those are shipped in the
|
||||
<literal>gerrit.war</literal> file.
|
||||
'';
|
||||
};
|
||||
|
||||
serverId = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Set a UUID that uniquely identifies the server.
|
||||
|
||||
This can be generated with
|
||||
<literal>nix-shell -p util-linux --run uuidgen</literal>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.replicationSettings != {} -> elem "replication" cfg.builtinPlugins;
|
||||
message = "Gerrit replicationSettings require enabling the replication plugin";
|
||||
}
|
||||
];
|
||||
|
||||
services.gerrit.settings = {
|
||||
cache.directory = "/var/cache/gerrit";
|
||||
container.heapLimit = cfg.jvmHeapLimit;
|
||||
gerrit.basePath = lib.mkDefault "git";
|
||||
gerrit.serverId = cfg.serverId;
|
||||
httpd.inheritChannel = "true";
|
||||
httpd.listenUrl = lib.mkDefault "http://${cfg.listenAddress}";
|
||||
index.type = lib.mkDefault "lucene";
|
||||
};
|
||||
|
||||
# Add the gerrit CLI to the system to run `gerrit init` and friends.
|
||||
environment.systemPackages = [ gerrit-cli ];
|
||||
|
||||
systemd.sockets.gerrit = {
|
||||
unitConfig.Description = "Gerrit HTTP socket";
|
||||
wantedBy = [ "sockets.target" ];
|
||||
listenStreams = [ cfg.listenAddress ];
|
||||
};
|
||||
|
||||
systemd.services.gerrit = {
|
||||
description = "Gerrit";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "gerrit.socket" ];
|
||||
after = [ "gerrit.socket" "network.target" ];
|
||||
|
||||
path = [
|
||||
gerrit-cli
|
||||
pkgs.bash
|
||||
pkgs.coreutils
|
||||
pkgs.git
|
||||
pkgs.openssh
|
||||
];
|
||||
|
||||
environment = {
|
||||
GERRIT_HOME = "%S/gerrit";
|
||||
GERRIT_TMP = "%T";
|
||||
HOME = "%S/gerrit";
|
||||
XDG_CONFIG_HOME = "%S/gerrit/.config";
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
set -euo pipefail
|
||||
|
||||
# bootstrap if nothing exists
|
||||
if [[ ! -d git ]]; then
|
||||
gerrit init --batch --no-auto-start
|
||||
fi
|
||||
|
||||
# install gerrit.war for the plugin manager
|
||||
rm -rf bin
|
||||
mkdir bin
|
||||
ln -sfv ${cfg.package}/webapps/${cfg.package.name}.war bin/gerrit.war
|
||||
|
||||
# copy the config, keep it mutable because Gerrit
|
||||
ln -sfv ${gerritConfig} etc/gerrit.config
|
||||
ln -sfv ${replicationConfig} etc/replication.config
|
||||
|
||||
# install the plugins
|
||||
rm -rf plugins
|
||||
ln -sv ${gerrit-plugins} plugins
|
||||
''
|
||||
;
|
||||
|
||||
serviceConfig = {
|
||||
CacheDirectory = "gerrit";
|
||||
DynamicUser = true;
|
||||
ExecStart = "${gerrit-cli}/bin/gerrit daemon --console-log";
|
||||
LimitNOFILE = 4096;
|
||||
StandardInput = "socket";
|
||||
StandardOutput = "journal";
|
||||
StateDirectory = "gerrit";
|
||||
WorkingDirectory = "%S/gerrit";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ edef zimbatm ];
|
||||
# uses attributes of the linked package
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
49
nixos/modules/services/web-apps/gotify-server.nix
Normal file
49
nixos/modules/services/web-apps/gotify-server.nix
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{ pkgs, lib, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.gotify;
|
||||
in {
|
||||
options = {
|
||||
services.gotify = {
|
||||
enable = mkEnableOption "Gotify webserver";
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
description = ''
|
||||
Port the server listens to.
|
||||
'';
|
||||
};
|
||||
|
||||
stateDirectoryName = mkOption {
|
||||
type = types.str;
|
||||
default = "gotify-server";
|
||||
description = ''
|
||||
The name of the directory below <filename>/var/lib</filename> where
|
||||
gotify stores its runtime data.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.gotify-server = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
description = "Simple server for sending and receiving messages";
|
||||
|
||||
environment = {
|
||||
GOTIFY_SERVER_PORT = toString cfg.port;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
|
||||
StateDirectory = cfg.stateDirectoryName;
|
||||
Restart = "always";
|
||||
DynamicUser = "yes";
|
||||
ExecStart = "${pkgs.gotify-server}/bin/server";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
172
nixos/modules/services/web-apps/grocy.nix
Normal file
172
nixos/modules/services/web-apps/grocy.nix
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.grocy;
|
||||
in {
|
||||
options.services.grocy = {
|
||||
enable = mkEnableOption "grocy";
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
FQDN for the grocy instance.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx.enableSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether or not to enable SSL (with ACME and let's encrypt)
|
||||
for the grocy vhost.
|
||||
'';
|
||||
};
|
||||
|
||||
phpfpm.settings = mkOption {
|
||||
type = with types; attrsOf (oneOf [ int str bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"php_admin_value[error_log]" = "stderr";
|
||||
"php_admin_flag[log_errors]" = true;
|
||||
"listen.owner" = "nginx";
|
||||
"catch_workers_output" = true;
|
||||
"pm.max_children" = "32";
|
||||
"pm.start_servers" = "2";
|
||||
"pm.min_spare_servers" = "2";
|
||||
"pm.max_spare_servers" = "4";
|
||||
"pm.max_requests" = "500";
|
||||
};
|
||||
|
||||
description = ''
|
||||
Options for grocy's PHPFPM pool.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/grocy";
|
||||
description = ''
|
||||
Home directory of the <literal>grocy</literal> user which contains
|
||||
the application's state.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = {
|
||||
currency = mkOption {
|
||||
type = types.str;
|
||||
default = "USD";
|
||||
example = "EUR";
|
||||
description = ''
|
||||
ISO 4217 code for the currency to display.
|
||||
'';
|
||||
};
|
||||
|
||||
culture = mkOption {
|
||||
type = types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
|
||||
default = "en";
|
||||
description = ''
|
||||
Display language of the frontend.
|
||||
'';
|
||||
};
|
||||
|
||||
calendar = {
|
||||
showWeekNumber = mkOption {
|
||||
default = true;
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Show the number of the weeks in the calendar views.
|
||||
'';
|
||||
};
|
||||
firstDayOfWeek = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr (types.enum (range 0 6));
|
||||
description = ''
|
||||
Which day of the week (0=Sunday, 1=Monday etc.) should be the
|
||||
first day.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.etc."grocy/config.php".text = ''
|
||||
<?php
|
||||
Setting('CULTURE', '${cfg.settings.culture}');
|
||||
Setting('CURRENCY', '${cfg.settings.currency}');
|
||||
Setting('CALENDAR_FIRST_DAY_OF_WEEK', '${toString cfg.settings.calendar.firstDayOfWeek}');
|
||||
Setting('CALENDAR_SHOW_WEEK_OF_YEAR', ${boolToString cfg.settings.calendar.showWeekNumber});
|
||||
'';
|
||||
|
||||
users.users.grocy = {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = cfg.dataDir;
|
||||
group = "nginx";
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = map (
|
||||
dirName: "d '${cfg.dataDir}/${dirName}' - grocy nginx - -"
|
||||
) [ "viewcache" "plugins" "settingoverrides" "storage" ];
|
||||
|
||||
services.phpfpm.pools.grocy = {
|
||||
user = "grocy";
|
||||
group = "nginx";
|
||||
|
||||
# PHP 7.4 is the only version which is supported/tested by upstream:
|
||||
# https://github.com/grocy/grocy/blob/v3.0.0/README.md#how-to-install
|
||||
phpPackage = pkgs.php74;
|
||||
|
||||
inherit (cfg.phpfpm) settings;
|
||||
|
||||
phpEnv = {
|
||||
GROCY_CONFIG_FILE = "/etc/grocy/config.php";
|
||||
GROCY_DB_FILE = "${cfg.dataDir}/grocy.db";
|
||||
GROCY_STORAGE_DIR = "${cfg.dataDir}/storage";
|
||||
GROCY_PLUGIN_DIR = "${cfg.dataDir}/plugins";
|
||||
GROCY_CACHE_DIR = "${cfg.dataDir}/viewcache";
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.hostName}" = mkMerge [
|
||||
{ root = "${pkgs.grocy}/public";
|
||||
locations."/".extraConfig = ''
|
||||
rewrite ^ /index.php;
|
||||
'';
|
||||
locations."~ \\.php$".extraConfig = ''
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.grocy.socket};
|
||||
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
'';
|
||||
locations."~ \\.(js|css|ttf|woff2?|png|jpe?g|svg)$".extraConfig = ''
|
||||
add_header Cache-Control "public, max-age=15778463";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
add_header X-Download-Options noopen;
|
||||
add_header X-Permitted-Cross-Domain-Policies none;
|
||||
add_header Referrer-Policy no-referrer;
|
||||
access_log off;
|
||||
'';
|
||||
extraConfig = ''
|
||||
try_files $uri /index.php;
|
||||
'';
|
||||
}
|
||||
(mkIf cfg.nginx.enableSSL {
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
})
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
meta = {
|
||||
maintainers = with maintainers; [ ma27 ];
|
||||
doc = ./grocy.xml;
|
||||
};
|
||||
}
|
||||
77
nixos/modules/services/web-apps/grocy.xml
Normal file
77
nixos/modules/services/web-apps/grocy.xml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-grocy">
|
||||
|
||||
<title>Grocy</title>
|
||||
<para>
|
||||
<link xlink:href="https://grocy.info/">Grocy</link> is a web-based self-hosted groceries
|
||||
& household management solution for your home.
|
||||
</para>
|
||||
|
||||
<section xml:id="module-services-grocy-basic-usage">
|
||||
<title>Basic usage</title>
|
||||
<para>
|
||||
A very basic configuration may look like this:
|
||||
<programlisting>{ pkgs, ... }:
|
||||
{
|
||||
services.grocy = {
|
||||
<link linkend="opt-services.grocy.enable">enable</link> = true;
|
||||
<link linkend="opt-services.grocy.hostName">hostName</link> = "grocy.tld";
|
||||
};
|
||||
}</programlisting>
|
||||
This configures a simple vhost using <link linkend="opt-services.nginx.enable">nginx</link>
|
||||
which listens to <literal>grocy.tld</literal> with fully configured ACME/LE (this can be
|
||||
disabled by setting <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
|
||||
to <literal>false</literal>). After the initial setup the credentials <literal>admin:admin</literal>
|
||||
can be used to login.
|
||||
</para>
|
||||
<para>
|
||||
The application's state is persisted at <literal>/var/lib/grocy/grocy.db</literal> in a
|
||||
<package>sqlite3</package> database. The migration is applied when requesting the <literal>/</literal>-route
|
||||
of the application.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-grocy-settings">
|
||||
<title>Settings</title>
|
||||
<para>
|
||||
The configuration for <literal>grocy</literal> is located at <literal>/etc/grocy/config.php</literal>.
|
||||
By default, the following settings can be defined in the NixOS-configuration:
|
||||
<programlisting>{ pkgs, ... }:
|
||||
{
|
||||
services.grocy.settings = {
|
||||
# The default currency in the system for invoices etc.
|
||||
# Please note that exchange rates aren't taken into account, this
|
||||
# is just the setting for what's shown in the frontend.
|
||||
<link linkend="opt-services.grocy.settings.currency">currency</link> = "EUR";
|
||||
|
||||
# The display language (and locale configuration) for grocy.
|
||||
<link linkend="opt-services.grocy.settings.currency">culture</link> = "de";
|
||||
|
||||
calendar = {
|
||||
# Whether or not to show the week-numbers
|
||||
# in the calendar.
|
||||
<link linkend="opt-services.grocy.settings.calendar.showWeekNumber">showWeekNumber</link> = true;
|
||||
|
||||
# Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
|
||||
# 2=Tuesday and so on).
|
||||
<link linkend="opt-services.grocy.settings.calendar.firstDayOfWeek">firstDayOfWeek</link> = 2;
|
||||
};
|
||||
};
|
||||
}</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
If you want to alter the configuration file on your own, you can do this manually with
|
||||
an expression like this:
|
||||
<programlisting>{ lib, ... }:
|
||||
{
|
||||
environment.etc."grocy/config.php".text = lib.mkAfter ''
|
||||
// Arbitrary PHP code in grocy's configuration file
|
||||
'';
|
||||
}</programlisting>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
</chapter>
|
||||
1047
nixos/modules/services/web-apps/hedgedoc.nix
Normal file
1047
nixos/modules/services/web-apps/hedgedoc.nix
Normal file
File diff suppressed because it is too large
Load diff
142
nixos/modules/services/web-apps/hledger-web.nix
Normal file
142
nixos/modules/services/web-apps/hledger-web.nix
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.hledger-web;
|
||||
in {
|
||||
options.services.hledger-web = {
|
||||
|
||||
enable = mkEnableOption "hledger-web service";
|
||||
|
||||
serveApi = mkEnableOption "Serve only the JSON web API, without the web UI.";
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = ''
|
||||
Address to listen on.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5000;
|
||||
example = 80;
|
||||
description = ''
|
||||
Port to listen on.
|
||||
'';
|
||||
};
|
||||
|
||||
capabilities = {
|
||||
view = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Enable the view capability.
|
||||
'';
|
||||
};
|
||||
add = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable the add capability.
|
||||
'';
|
||||
};
|
||||
manage = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable the manage capability.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/hledger-web";
|
||||
description = ''
|
||||
Path the service has access to. If left as the default value this
|
||||
directory will automatically be created before the hledger-web server
|
||||
starts, otherwise the sysadmin is responsible for ensuring the
|
||||
directory exists with appropriate ownership and permissions.
|
||||
'';
|
||||
};
|
||||
|
||||
journalFiles = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ".hledger.journal" ];
|
||||
description = ''
|
||||
Paths to journal files relative to <option>services.hledger-web.stateDir</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
baseUrl = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "https://example.org";
|
||||
description = ''
|
||||
Base URL, when sharing over a network.
|
||||
'';
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "--forecast" ];
|
||||
description = ''
|
||||
Extra command line arguments to pass to hledger-web.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
users.users.hledger = {
|
||||
name = "hledger";
|
||||
group = "hledger";
|
||||
isSystemUser = true;
|
||||
home = cfg.stateDir;
|
||||
useDefaultShell = true;
|
||||
};
|
||||
|
||||
users.groups.hledger = {};
|
||||
|
||||
systemd.services.hledger-web = let
|
||||
capabilityString = with cfg.capabilities; concatStringsSep "," (
|
||||
(optional view "view")
|
||||
++ (optional add "add")
|
||||
++ (optional manage "manage")
|
||||
);
|
||||
serverArgs = with cfg; escapeShellArgs ([
|
||||
"--serve"
|
||||
"--host=${host}"
|
||||
"--port=${toString port}"
|
||||
"--capabilities=${capabilityString}"
|
||||
(optionalString (cfg.baseUrl != null) "--base-url=${cfg.baseUrl}")
|
||||
(optionalString (cfg.serveApi) "--serve-api")
|
||||
] ++ (map (f: "--file=${stateDir}/${f}") cfg.journalFiles)
|
||||
++ extraOptions);
|
||||
in {
|
||||
description = "hledger-web - web-app for the hledger accounting tool.";
|
||||
documentation = [ "https://hledger.org/hledger-web.html" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
serviceConfig = mkMerge [
|
||||
{
|
||||
ExecStart = "${pkgs.hledger-web}/bin/hledger-web ${serverArgs}";
|
||||
Restart = "always";
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
User = "hledger";
|
||||
Group = "hledger";
|
||||
PrivateTmp = true;
|
||||
}
|
||||
(mkIf (cfg.stateDir == "/var/lib/hledger-web") {
|
||||
StateDirectory = "hledger-web";
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ marijanp erictapen ];
|
||||
}
|
||||
262
nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
Normal file
262
nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
{ config, lib, pkgs, ... }: with lib; let
|
||||
cfg = config.services.icingaweb2;
|
||||
fpm = config.services.phpfpm.pools.${poolName};
|
||||
poolName = "icingaweb2";
|
||||
|
||||
defaultConfig = {
|
||||
global = {
|
||||
module_path = "${pkgs.icingaweb2}/modules";
|
||||
};
|
||||
};
|
||||
in {
|
||||
meta.maintainers = with maintainers; [ das_j ];
|
||||
|
||||
options.services.icingaweb2 = with types; {
|
||||
enable = mkEnableOption "the icingaweb2 web interface";
|
||||
|
||||
pool = mkOption {
|
||||
type = str;
|
||||
default = poolName;
|
||||
description = ''
|
||||
Name of existing PHP-FPM pool that is used to run Icingaweb2.
|
||||
If not specified, a pool will automatically created with default values.
|
||||
'';
|
||||
};
|
||||
|
||||
libraryPaths = mkOption {
|
||||
type = attrsOf package;
|
||||
default = { };
|
||||
description = ''
|
||||
Libraries to add to the Icingaweb2 library path.
|
||||
The name of the attribute is the name of the library, the value
|
||||
is the package to add.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = nullOr str;
|
||||
default = "icingaweb2";
|
||||
description = ''
|
||||
Name of the nginx virtualhost to use and setup. If null, no virtualhost is set up.
|
||||
'';
|
||||
};
|
||||
|
||||
timezone = mkOption {
|
||||
type = str;
|
||||
default = "UTC";
|
||||
example = "Europe/Berlin";
|
||||
description = "PHP-compliant timezone specification";
|
||||
};
|
||||
|
||||
modules = {
|
||||
doc.enable = mkEnableOption "the icingaweb2 doc module";
|
||||
migrate.enable = mkEnableOption "the icingaweb2 migrate module";
|
||||
setup.enable = mkEnableOption "the icingaweb2 setup module";
|
||||
test.enable = mkEnableOption "the icingaweb2 test module";
|
||||
translation.enable = mkEnableOption "the icingaweb2 translation module";
|
||||
};
|
||||
|
||||
modulePackages = mkOption {
|
||||
type = attrsOf package;
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
"snow" = icingaweb2Modules.theme-snow;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Name-package attrset of Icingaweb 2 modules packages to enable.
|
||||
|
||||
If you enable modules manually (e.g. via the web ui), they will not be touched.
|
||||
'';
|
||||
};
|
||||
|
||||
generalConfig = mkOption {
|
||||
type = nullOr attrs;
|
||||
default = null;
|
||||
example = {
|
||||
general = {
|
||||
showStacktraces = 1;
|
||||
config_resource = "icingaweb_db";
|
||||
};
|
||||
logging = {
|
||||
log = "syslog";
|
||||
level = "CRITICAL";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
config.ini contents.
|
||||
Will automatically be converted to a .ini file.
|
||||
If you don't set global.module_path, the module will take care of it.
|
||||
|
||||
If the value is null, no config.ini is created and you can
|
||||
modify it manually (e.g. via the web interface).
|
||||
Note that you need to update module_path manually.
|
||||
'';
|
||||
};
|
||||
|
||||
resources = mkOption {
|
||||
type = nullOr attrs;
|
||||
default = null;
|
||||
example = {
|
||||
icingaweb_db = {
|
||||
type = "db";
|
||||
db = "mysql";
|
||||
host = "localhost";
|
||||
username = "icingaweb2";
|
||||
password = "icingaweb2";
|
||||
dbname = "icingaweb2";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
resources.ini contents.
|
||||
Will automatically be converted to a .ini file.
|
||||
|
||||
If the value is null, no resources.ini is created and you can
|
||||
modify it manually (e.g. via the web interface).
|
||||
Note that if you set passwords here, they will go into the nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
authentications = mkOption {
|
||||
type = nullOr attrs;
|
||||
default = null;
|
||||
example = {
|
||||
icingaweb = {
|
||||
backend = "db";
|
||||
resource = "icingaweb_db";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
authentication.ini contents.
|
||||
Will automatically be converted to a .ini file.
|
||||
|
||||
If the value is null, no authentication.ini is created and you can
|
||||
modify it manually (e.g. via the web interface).
|
||||
'';
|
||||
};
|
||||
|
||||
groupBackends = mkOption {
|
||||
type = nullOr attrs;
|
||||
default = null;
|
||||
example = {
|
||||
icingaweb = {
|
||||
backend = "db";
|
||||
resource = "icingaweb_db";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
groups.ini contents.
|
||||
Will automatically be converted to a .ini file.
|
||||
|
||||
If the value is null, no groups.ini is created and you can
|
||||
modify it manually (e.g. via the web interface).
|
||||
'';
|
||||
};
|
||||
|
||||
roles = mkOption {
|
||||
type = nullOr attrs;
|
||||
default = null;
|
||||
example = {
|
||||
Administrators = {
|
||||
users = "admin";
|
||||
permissions = "*";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
roles.ini contents.
|
||||
Will automatically be converted to a .ini file.
|
||||
|
||||
If the value is null, no roles.ini is created and you can
|
||||
modify it manually (e.g. via the web interface).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
|
||||
${poolName} = {
|
||||
user = "icingaweb2";
|
||||
phpEnv = {
|
||||
ICINGAWEB_LIBDIR = toString (pkgs.linkFarm "icingaweb2-libdir" (mapAttrsToList (name: path: { inherit name path; }) cfg.libraryPaths));
|
||||
};
|
||||
phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
|
||||
phpOptions = ''
|
||||
date.timezone = "${cfg.timezone}"
|
||||
'';
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0600";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 10;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.icingaweb2.libraryPaths = {
|
||||
ipl = pkgs.icingaweb2-ipl;
|
||||
thirdparty = pkgs.icingaweb2-thirdparty;
|
||||
};
|
||||
|
||||
systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = mkIf (cfg.virtualHost != null) {
|
||||
${cfg.virtualHost} = {
|
||||
root = "${pkgs.icingaweb2}/public";
|
||||
|
||||
extraConfig = ''
|
||||
index index.php;
|
||||
try_files $1 $uri $uri/ /index.php$is_args$args;
|
||||
'';
|
||||
|
||||
locations."~ ..*/.*.php$".extraConfig = ''
|
||||
return 403;
|
||||
'';
|
||||
|
||||
locations."~ ^/index.php(.*)$".extraConfig = ''
|
||||
fastcgi_intercept_errors on;
|
||||
fastcgi_index index.php;
|
||||
include ${config.services.nginx.package}/conf/fastcgi.conf;
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
fastcgi_param SCRIPT_FILENAME ${pkgs.icingaweb2}/public/index.php;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# /etc/icingaweb2
|
||||
environment.etc = let
|
||||
doModule = name: optionalAttrs (cfg.modules.${name}.enable) { "icingaweb2/enabledModules/${name}".source = "${pkgs.icingaweb2}/modules/${name}"; };
|
||||
in {}
|
||||
# Module packages
|
||||
// (mapAttrs' (k: v: nameValuePair "icingaweb2/enabledModules/${k}" { source = v; }) cfg.modulePackages)
|
||||
# Built-in modules
|
||||
// doModule "doc"
|
||||
// doModule "migrate"
|
||||
// doModule "setup"
|
||||
// doModule "test"
|
||||
// doModule "translation"
|
||||
# Configs
|
||||
// optionalAttrs (cfg.generalConfig != null) { "icingaweb2/config.ini".text = generators.toINI {} (defaultConfig // cfg.generalConfig); }
|
||||
// optionalAttrs (cfg.resources != null) { "icingaweb2/resources.ini".text = generators.toINI {} cfg.resources; }
|
||||
// optionalAttrs (cfg.authentications != null) { "icingaweb2/authentication.ini".text = generators.toINI {} cfg.authentications; }
|
||||
// optionalAttrs (cfg.groupBackends != null) { "icingaweb2/groups.ini".text = generators.toINI {} cfg.groupBackends; }
|
||||
// optionalAttrs (cfg.roles != null) { "icingaweb2/roles.ini".text = generators.toINI {} cfg.roles; };
|
||||
|
||||
# User and group
|
||||
users.groups.icingaweb2 = {};
|
||||
users.users.icingaweb2 = {
|
||||
description = "Icingaweb2 service user";
|
||||
group = "icingaweb2";
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
157
nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
Normal file
157
nixos/modules/services/web-apps/icingaweb2/module-monitoring.nix
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
{ config, lib, pkgs, ... }: with lib; let
|
||||
cfg = config.services.icingaweb2.modules.monitoring;
|
||||
|
||||
configIni = ''
|
||||
[security]
|
||||
protected_customvars = "${concatStringsSep "," cfg.generalConfig.protectedVars}"
|
||||
'';
|
||||
|
||||
backendsIni = let
|
||||
formatBool = b: if b then "1" else "0";
|
||||
in concatStringsSep "\n" (mapAttrsToList (name: config: ''
|
||||
[${name}]
|
||||
type = "ido"
|
||||
resource = "${config.resource}"
|
||||
disabled = "${formatBool config.disabled}"
|
||||
'') cfg.backends);
|
||||
|
||||
transportsIni = concatStringsSep "\n" (mapAttrsToList (name: config: ''
|
||||
[${name}]
|
||||
type = "${config.type}"
|
||||
${optionalString (config.instance != null) ''instance = "${config.instance}"''}
|
||||
${optionalString (config.type == "local" || config.type == "remote") ''path = "${config.path}"''}
|
||||
${optionalString (config.type != "local") ''
|
||||
host = "${config.host}"
|
||||
${optionalString (config.port != null) ''port = "${toString config.port}"''}
|
||||
user${optionalString (config.type == "api") "name"} = "${config.username}"
|
||||
''}
|
||||
${optionalString (config.type == "api") ''password = "${config.password}"''}
|
||||
${optionalString (config.type == "remote") ''resource = "${config.resource}"''}
|
||||
'') cfg.transports);
|
||||
|
||||
in {
|
||||
options.services.icingaweb2.modules.monitoring = with types; {
|
||||
enable = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = "Whether to enable the icingaweb2 monitoring module.";
|
||||
};
|
||||
|
||||
generalConfig = {
|
||||
mutable = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = "Make config.ini of the monitoring module mutable (e.g. via the web interface).";
|
||||
};
|
||||
|
||||
protectedVars = mkOption {
|
||||
type = listOf str;
|
||||
default = [ "*pw*" "*pass*" "community" ];
|
||||
description = "List of string patterns for custom variables which should be excluded from user’s view.";
|
||||
};
|
||||
};
|
||||
|
||||
mutableBackends = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = "Make backends.ini of the monitoring module mutable (e.g. via the web interface).";
|
||||
};
|
||||
|
||||
backends = mkOption {
|
||||
default = { icinga = { resource = "icinga_ido"; }; };
|
||||
description = "Monitoring backends to define";
|
||||
type = attrsOf (submodule ({ name, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
visible = false;
|
||||
default = name;
|
||||
type = str;
|
||||
description = "Name of this backend";
|
||||
};
|
||||
|
||||
resource = mkOption {
|
||||
type = str;
|
||||
description = "Name of the IDO resource";
|
||||
};
|
||||
|
||||
disabled = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = "Disable this backend";
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
|
||||
mutableTransports = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = "Make commandtransports.ini of the monitoring module mutable (e.g. via the web interface).";
|
||||
};
|
||||
|
||||
transports = mkOption {
|
||||
default = {};
|
||||
description = "Command transports to define";
|
||||
type = attrsOf (submodule ({ name, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
visible = false;
|
||||
default = name;
|
||||
type = str;
|
||||
description = "Name of this transport";
|
||||
};
|
||||
|
||||
type = mkOption {
|
||||
type = enum [ "api" "local" "remote" ];
|
||||
default = "api";
|
||||
description = "Type of this transport";
|
||||
};
|
||||
|
||||
instance = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
description = "Assign a icinga instance to this transport";
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = str;
|
||||
description = "Path to the socket for local or remote transports";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = str;
|
||||
description = "Host for the api or remote transport";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
description = "Port to connect to for the api or remote transport";
|
||||
};
|
||||
|
||||
username = mkOption {
|
||||
type = str;
|
||||
description = "Username for the api or remote transport";
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = str;
|
||||
description = "Password for the api transport";
|
||||
};
|
||||
|
||||
resource = mkOption {
|
||||
type = str;
|
||||
description = "SSH identity resource for the remote transport";
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (config.services.icingaweb2.enable && cfg.enable) {
|
||||
environment.etc = { "icingaweb2/enabledModules/monitoring" = { source = "${pkgs.icingaweb2}/modules/monitoring"; }; }
|
||||
// optionalAttrs (!cfg.generalConfig.mutable) { "icingaweb2/modules/monitoring/config.ini".text = configIni; }
|
||||
// optionalAttrs (!cfg.mutableBackends) { "icingaweb2/modules/monitoring/backends.ini".text = backendsIni; }
|
||||
// optionalAttrs (!cfg.mutableTransports) { "icingaweb2/modules/monitoring/commandtransports.ini".text = transportsIni; };
|
||||
};
|
||||
}
|
||||
153
nixos/modules/services/web-apps/ihatemoney/default.nix
Normal file
153
nixos/modules/services/web-apps/ihatemoney/default.nix
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.ihatemoney;
|
||||
user = "ihatemoney";
|
||||
group = "ihatemoney";
|
||||
db = "ihatemoney";
|
||||
python3 = config.services.uwsgi.package.python3;
|
||||
pkg = python3.pkgs.ihatemoney;
|
||||
toBool = x: if x then "True" else "False";
|
||||
configFile = pkgs.writeText "ihatemoney.cfg" ''
|
||||
from secrets import token_hex
|
||||
# load a persistent secret key
|
||||
SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
|
||||
SECRET_KEY = ""
|
||||
try:
|
||||
with open(SECRET_KEY_FILE) as f:
|
||||
SECRET_KEY = f.read()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if not SECRET_KEY:
|
||||
print("ihatemoney: generating a new secret key")
|
||||
SECRET_KEY = token_hex(50)
|
||||
with open(SECRET_KEY_FILE, "w") as f:
|
||||
f.write(SECRET_KEY)
|
||||
del token_hex
|
||||
del SECRET_KEY_FILE
|
||||
|
||||
# "normal" configuration
|
||||
DEBUG = False
|
||||
SQLALCHEMY_DATABASE_URI = '${
|
||||
if cfg.backend == "sqlite"
|
||||
then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
|
||||
else "postgresql:///${db}"}'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}")
|
||||
ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
|
||||
ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}"
|
||||
ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
|
||||
ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
|
||||
SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie}
|
||||
ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha}
|
||||
LEGAL_LINK = r"${toString cfg.legalLink}"
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.ihatemoney = {
|
||||
enable = mkEnableOption "ihatemoney webapp. Note that this will set uwsgi to emperor mode";
|
||||
backend = mkOption {
|
||||
type = types.enum [ "sqlite" "postgresql" ];
|
||||
default = "sqlite";
|
||||
description = ''
|
||||
The database engine to use for ihatemoney.
|
||||
If <literal>postgresql</literal> is selected, then a database called
|
||||
<literal>${db}</literal> will be created. If you disable this option,
|
||||
it will however not be removed.
|
||||
'';
|
||||
};
|
||||
adminHashedPassword = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The hashed password of the administrator. To obtain it, run <literal>ihatemoney generate_password_hash</literal>";
|
||||
};
|
||||
uwsgiConfig = mkOption {
|
||||
type = types.attrs;
|
||||
example = {
|
||||
http = ":8000";
|
||||
};
|
||||
description = "Additionnal configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
|
||||
};
|
||||
defaultSender = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "Budget manager";
|
||||
description = "The display name of the sender of ihatemoney emails";
|
||||
};
|
||||
email = mkOption {
|
||||
type = types.str;
|
||||
default = "ihatemoney@${config.networking.hostName}";
|
||||
defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"'';
|
||||
description = "The email of the sender of ihatemoney emails";
|
||||
};
|
||||
};
|
||||
secureCookie = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
|
||||
};
|
||||
enableDemoProject = mkEnableOption "access to the demo project in ihatemoney";
|
||||
enablePublicProjectCreation = mkEnableOption "permission to create projects in ihatemoney by anyone";
|
||||
enableAdminDashboard = mkEnableOption "ihatemoney admin dashboard";
|
||||
enableCaptcha = mkEnableOption "a simplistic captcha for some forms";
|
||||
legalLink = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
|
||||
};
|
||||
extraConfig = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
|
||||
};
|
||||
};
|
||||
config = mkIf cfg.enable {
|
||||
services.postgresql = mkIf (cfg.backend == "postgresql") {
|
||||
enable = true;
|
||||
ensureDatabases = [ db ];
|
||||
ensureUsers = [ {
|
||||
name = user;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${db}" = "ALL PRIVILEGES";
|
||||
};
|
||||
} ];
|
||||
};
|
||||
systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
|
||||
wantedBy = [ "uwsgi.service" ];
|
||||
before = [ "uwsgi.service" ];
|
||||
};
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/ihatemoney 770 ${user} ${group}"
|
||||
];
|
||||
users = {
|
||||
users.${user} = {
|
||||
isSystemUser = true;
|
||||
inherit group;
|
||||
};
|
||||
groups.${group} = {};
|
||||
};
|
||||
services.uwsgi = {
|
||||
enable = true;
|
||||
plugins = [ "python3" ];
|
||||
instance = {
|
||||
type = "emperor";
|
||||
vassals.ihatemoney = {
|
||||
type = "normal";
|
||||
strict = true;
|
||||
immediate-uid = user;
|
||||
immediate-gid = group;
|
||||
# apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
|
||||
enable-threads = true;
|
||||
module = "wsgi:application";
|
||||
chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
|
||||
env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
|
||||
pythonPackages = self: [ self.ihatemoney ];
|
||||
} // cfg.uwsgiConfig;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
264
nixos/modules/services/web-apps/invidious.nix
Normal file
264
nixos/modules/services/web-apps/invidious.nix
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
{ lib, config, pkgs, options, ... }:
|
||||
let
|
||||
cfg = config.services.invidious;
|
||||
# To allow injecting secrets with jq, json (instead of yaml) is used
|
||||
settingsFormat = pkgs.formats.json { };
|
||||
inherit (lib) types;
|
||||
|
||||
settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
|
||||
|
||||
serviceConfig = {
|
||||
systemd.services.invidious = {
|
||||
description = "Invidious (An alternative YouTube front-end)";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
script =
|
||||
let
|
||||
jqFilter = "."
|
||||
+ lib.optionalString (cfg.database.host != null) "[0].db.password = \"'\"'\"$(cat ${lib.escapeShellArg cfg.database.passwordFile})\"'\"'\""
|
||||
+ " | .[0]"
|
||||
+ lib.optionalString (cfg.extraSettingsFile != null) " * .[1]";
|
||||
jqFiles = [ settingsFile ] ++ lib.optional (cfg.extraSettingsFile != null) cfg.extraSettingsFile;
|
||||
in
|
||||
''
|
||||
export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s "${jqFilter}" ${lib.escapeShellArgs jqFiles})"
|
||||
exec ${cfg.package}/bin/invidious
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
RestartSec = "2s";
|
||||
DynamicUser = true;
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectHome = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
};
|
||||
};
|
||||
|
||||
services.invidious.settings = {
|
||||
inherit (cfg) port;
|
||||
|
||||
# Automatically initialises and migrates the database if necessary
|
||||
check_tables = true;
|
||||
|
||||
db = {
|
||||
user = lib.mkDefault "kemal";
|
||||
dbname = lib.mkDefault "invidious";
|
||||
port = cfg.database.port;
|
||||
# Blank for unix sockets, see
|
||||
# https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
|
||||
host = if cfg.database.host == null then "" else cfg.database.host;
|
||||
# Not needed because peer authentication is enabled
|
||||
password = lib.mkIf (cfg.database.host == null) "";
|
||||
};
|
||||
} // (lib.optionalAttrs (cfg.domain != null) {
|
||||
inherit (cfg) domain;
|
||||
});
|
||||
|
||||
assertions = [{
|
||||
assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
|
||||
message = "If database host isn't null, database password needs to be set";
|
||||
}];
|
||||
};
|
||||
|
||||
# Settings necessary for running with an automatically managed local database
|
||||
localDatabaseConfig = lib.mkIf cfg.database.createLocally {
|
||||
# Default to using the local database if we create it
|
||||
services.invidious.database.host = lib.mkDefault null;
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureDatabases = lib.singleton cfg.settings.db.dbname;
|
||||
ensureUsers = lib.singleton {
|
||||
name = cfg.settings.db.user;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${cfg.settings.db.dbname}" = "ALL PRIVILEGES";
|
||||
};
|
||||
};
|
||||
# This is only needed because the unix user invidious isn't the same as
|
||||
# the database user. This tells postgres to map one to the other.
|
||||
identMap = ''
|
||||
invidious invidious ${cfg.settings.db.user}
|
||||
'';
|
||||
# And this specifically enables peer authentication for only this
|
||||
# database, which allows passwordless authentication over the postgres
|
||||
# unix socket for the user map given above.
|
||||
authentication = ''
|
||||
local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.invidious-db-clean = {
|
||||
description = "Invidious database cleanup";
|
||||
documentation = [ "https://docs.invidious.io/Database-Information-and-Maintenance.md" ];
|
||||
startAt = lib.mkDefault "weekly";
|
||||
path = [ config.services.postgresql.package ];
|
||||
script = ''
|
||||
psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "DELETE FROM nonces * WHERE expire < current_timestamp"
|
||||
psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "TRUNCATE TABLE videos"
|
||||
'';
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
User = "invidious";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.invidious = {
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
User = "invidious";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nginxConfig = lib.mkIf cfg.nginx.enable {
|
||||
services.invidious.settings = {
|
||||
https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
|
||||
external_port = 80;
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.${cfg.domain} = {
|
||||
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
|
||||
enableACME = lib.mkDefault true;
|
||||
forceSSL = lib.mkDefault true;
|
||||
};
|
||||
};
|
||||
|
||||
assertions = [{
|
||||
assertion = cfg.domain != null;
|
||||
message = "To use services.invidious.nginx, you need to set services.invidious.domain";
|
||||
}];
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services.invidious = {
|
||||
enable = lib.mkEnableOption "Invidious";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.invidious;
|
||||
defaultText = "pkgs.invidious";
|
||||
description = "The Invidious package to use.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
description = ''
|
||||
The settings Invidious should use.
|
||||
|
||||
See <link xlink:href="https://github.com/iv-org/invidious/blob/master/config/config.example.yml">config.example.yml</link> for a list of all possible options.
|
||||
'';
|
||||
};
|
||||
|
||||
extraSettingsFile = lib.mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
A file including Invidious settings.
|
||||
|
||||
It gets merged with the setttings specified in <option>services.invidious.settings</option>
|
||||
and can be used to store secrets like <literal>hmac_key</literal> outside of the nix store.
|
||||
'';
|
||||
};
|
||||
|
||||
# This needs to be outside of settings to avoid infinite recursion
|
||||
# (determining if nginx should be enabled and therefore the settings
|
||||
# modified).
|
||||
domain = lib.mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The FQDN Invidious is reachable on.
|
||||
|
||||
This is used to configure nginx and for building absolute URLs.
|
||||
'';
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = types.port;
|
||||
# Default from https://docs.invidious.io/Configuration.md
|
||||
default = 3000;
|
||||
description = ''
|
||||
The port Invidious should listen on.
|
||||
|
||||
To allow access from outside,
|
||||
you can use either <option>services.invidious.nginx</option>
|
||||
or add <literal>config.services.invidious.port</literal> to <option>networking.firewall.allowedTCPPorts</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
createLocally = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to create a local database with PostgreSQL.
|
||||
'';
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The database host Invidious should use.
|
||||
|
||||
If <literal>null</literal>, the local unix socket is used. Otherwise
|
||||
TCP is used.
|
||||
'';
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = types.port;
|
||||
default = options.services.postgresql.port.default;
|
||||
defaultText = lib.literalExpression "options.services.postgresql.port.default";
|
||||
description = ''
|
||||
The port of the database Invidious should use.
|
||||
|
||||
Defaults to the the default postgresql port.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = types.nullOr types.str;
|
||||
apply = lib.mapNullable toString;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to file containing the database password.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
nginx.enable = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to configure nginx as a reverse proxy for Invidious.
|
||||
|
||||
It serves it under the domain specified in <option>services.invidious.settings.domain</option> with enabled TLS and ACME.
|
||||
Further configuration can be done through <option>services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*</option>,
|
||||
which can also be used to disable AMCE and TLS.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
serviceConfig
|
||||
localDatabaseConfig
|
||||
nginxConfig
|
||||
]);
|
||||
}
|
||||
305
nixos/modules/services/web-apps/invoiceplane.nix
Normal file
305
nixos/modules/services/web-apps/invoiceplane.nix
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.invoiceplane;
|
||||
eachSite = cfg.sites;
|
||||
user = "invoiceplane";
|
||||
webserver = config.services.${cfg.webserver};
|
||||
|
||||
invoiceplane-config = hostName: cfg: pkgs.writeText "ipconfig.php" ''
|
||||
IP_URL=http://${hostName}
|
||||
ENABLE_DEBUG=false
|
||||
DISABLE_SETUP=false
|
||||
REMOVE_INDEXPHP=false
|
||||
DB_HOSTNAME=${cfg.database.host}
|
||||
DB_USERNAME=${cfg.database.user}
|
||||
# NOTE: file_get_contents adds newline at the end of returned string
|
||||
DB_PASSWORD=${if cfg.database.passwordFile == null then "" else "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")"}
|
||||
DB_DATABASE=${cfg.database.name}
|
||||
DB_PORT=${toString cfg.database.port}
|
||||
SESS_EXPIRATION=864000
|
||||
ENABLE_INVOICE_DELETION=false
|
||||
DISABLE_READ_ONLY=false
|
||||
ENCRYPTION_KEY=
|
||||
ENCRYPTION_CIPHER=AES-256
|
||||
SETUP_COMPLETED=false
|
||||
'';
|
||||
|
||||
extraConfig = hostName: cfg: pkgs.writeText "extraConfig.php" ''
|
||||
${toString cfg.extraConfig}
|
||||
'';
|
||||
|
||||
pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
|
||||
pname = "invoiceplane-${hostName}";
|
||||
version = src.version;
|
||||
src = pkgs.invoiceplane;
|
||||
|
||||
patchPhase = ''
|
||||
# Patch index.php file to load additional config file
|
||||
substituteInPlace index.php \
|
||||
--replace "require('vendor/autoload.php');" "require('vendor/autoload.php'); \$dotenv = new \Dotenv\Dotenv(__DIR__, 'extraConfig.php'); \$dotenv->load();";
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r * $out/
|
||||
|
||||
# symlink uploads and log directories
|
||||
rm -r $out/uploads $out/application/logs $out/vendor/mpdf/mpdf/tmp
|
||||
ln -sf ${cfg.stateDir}/uploads $out/
|
||||
ln -sf ${cfg.stateDir}/logs $out/application/
|
||||
ln -sf ${cfg.stateDir}/tmp $out/vendor/mpdf/mpdf/
|
||||
|
||||
# symlink the InvoicePlane config
|
||||
ln -s ${cfg.stateDir}/ipconfig.php $out/ipconfig.php
|
||||
|
||||
# symlink the extraConfig file
|
||||
ln -s ${extraConfig hostName cfg} $out/extraConfig.php
|
||||
|
||||
# symlink additional templates
|
||||
${concatMapStringsSep "\n" (template: "cp -r ${template}/. $out/application/views/invoice_templates/pdf/") cfg.invoiceTemplates}
|
||||
'';
|
||||
};
|
||||
|
||||
siteOpts = { lib, name, ... }:
|
||||
{
|
||||
options = {
|
||||
|
||||
enable = mkEnableOption "InvoicePlane web application";
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/invoiceplane/${name}";
|
||||
description = ''
|
||||
This directory is used for uploads of attachements and cache.
|
||||
The directory passed here is automatically created and permissions
|
||||
adjusted as required.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "invoiceplane";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "invoiceplane";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/invoiceplane-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
invoiceTemplates = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
description = ''
|
||||
List of path(s) to respective template(s) which are copied from the 'invoice_templates/pdf' directory.
|
||||
<note><para>These templates need to be packaged before use, see example.</para></note>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
let
|
||||
# Let's package an example template
|
||||
template-vtdirektmarketing = pkgs.stdenv.mkDerivation {
|
||||
name = "vtdirektmarketing";
|
||||
# Download the template from a public repository
|
||||
src = pkgs.fetchgit {
|
||||
url = "https://git.project-insanity.org/onny/invoiceplane-vtdirektmarketing.git";
|
||||
sha256 = "1hh0q7wzsh8v8x03i82p6qrgbxr4v5fb05xylyrpp975l8axyg2z";
|
||||
};
|
||||
sourceRoot = ".";
|
||||
# Installing simply means copying template php file to the output directory
|
||||
installPhase = ""
|
||||
mkdir -p $out
|
||||
cp invoiceplane-vtdirektmarketing/vtdirektmarketing.php $out/
|
||||
"";
|
||||
};
|
||||
# And then pass this package to the template list like this:
|
||||
in [ template-vtdirektmarketing ]
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the InvoicePlane PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.nullOr types.lines;
|
||||
default = null;
|
||||
example = ''
|
||||
SETUP_COMPLETED=true
|
||||
DISABLE_SETUP=true
|
||||
IP_URL=https://invoice.example.com
|
||||
'';
|
||||
description = ''
|
||||
InvoicePlane configuration. Refer to
|
||||
<link xlink:href="https://github.com/InvoicePlane/InvoicePlane/blob/master/ipconfig.php.example"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
in
|
||||
{
|
||||
# interface
|
||||
options = {
|
||||
services.invoiceplane = mkOption {
|
||||
type = types.submodule {
|
||||
|
||||
options.sites = mkOption {
|
||||
type = types.attrsOf (types.submodule siteOpts);
|
||||
default = {};
|
||||
description = "Specification of one or more WordPress sites to serve";
|
||||
};
|
||||
|
||||
options.webserver = mkOption {
|
||||
type = types.enum [ "caddy" ];
|
||||
default = "caddy";
|
||||
description = ''
|
||||
Which webserver to use for virtual host management. Currently only
|
||||
caddy is supported.
|
||||
'';
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "InvoicePlane configuration.";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
# implementation
|
||||
config = mkIf (eachSite != {}) (mkMerge [{
|
||||
|
||||
assertions = flatten (mapAttrsToList (hostName: cfg:
|
||||
[{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
||||
message = ''services.invoiceplane.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = ''services.invoiceplane.sites."${hostName}".database.passwordFile cannot be specified if services.invoiceplane.sites."${hostName}".database.createLocally is set to true.'';
|
||||
}]
|
||||
) eachSite);
|
||||
|
||||
services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
|
||||
ensureUsers = mapAttrsToList (hostName: cfg:
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
) eachSite;
|
||||
};
|
||||
|
||||
services.phpfpm = {
|
||||
phpPackage = pkgs.php74;
|
||||
pools = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "invoiceplane-${hostName}" {
|
||||
inherit user;
|
||||
group = webserver.group;
|
||||
settings = {
|
||||
"listen.owner" = webserver.user;
|
||||
"listen.group" = webserver.group;
|
||||
} // cfg.poolConfig;
|
||||
}
|
||||
)) eachSite;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
|
||||
"d ${cfg.stateDir} 0750 ${user} ${webserver.group} - -"
|
||||
"f ${cfg.stateDir}/ipconfig.php 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/logs 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/uploads 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/uploads/archive 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/uploads/customer_files 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/uploads/temp 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/uploads/temp/mpdf 0750 ${user} ${webserver.group} - -"
|
||||
"d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -"
|
||||
]) eachSite);
|
||||
|
||||
systemd.services.invoiceplane-config = {
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = concatStrings (mapAttrsToList (hostName: cfg:
|
||||
''
|
||||
mkdir -p ${cfg.stateDir}/logs \
|
||||
${cfg.stateDir}/uploads
|
||||
if ! grep -q IP_URL "${cfg.stateDir}/ipconfig.php"; then
|
||||
cp "${invoiceplane-config hostName cfg}" "${cfg.stateDir}/ipconfig.php"
|
||||
fi
|
||||
'') eachSite);
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
users.users.${user} = {
|
||||
group = webserver.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
}
|
||||
|
||||
(mkIf (cfg.webserver == "caddy") {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "http://${hostName}" {
|
||||
extraConfig = ''
|
||||
root * ${pkg hostName cfg}
|
||||
file_server
|
||||
|
||||
php_fastcgi unix/${config.services.phpfpm.pools."invoiceplane-${hostName}".socket}
|
||||
'';
|
||||
}
|
||||
)) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
69
nixos/modules/services/web-apps/isso.nix
Normal file
69
nixos/modules/services/web-apps/isso.nix
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkEnableOption mkIf mkOption types literalExpression;
|
||||
|
||||
cfg = config.services.isso;
|
||||
|
||||
settingsFormat = pkgs.formats.ini { };
|
||||
configFile = settingsFormat.generate "isso.conf" cfg.settings;
|
||||
in {
|
||||
|
||||
options = {
|
||||
services.isso = {
|
||||
enable = mkEnableOption ''
|
||||
A commenting server similar to Disqus.
|
||||
|
||||
Note: The application's author suppose to run isso behind a reverse proxy.
|
||||
The embedded solution offered by NixOS is also only suitable for small installations
|
||||
below 20 requests per second.
|
||||
'';
|
||||
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Configuration for <package>isso</package>.
|
||||
|
||||
See <link xlink:href="https://posativ.org/isso/docs/configuration/server/">Isso Server Configuration</link>
|
||||
for supported values.
|
||||
'';
|
||||
|
||||
type = types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
};
|
||||
|
||||
example = literalExpression ''
|
||||
{
|
||||
general = {
|
||||
host = "http://localhost";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.isso.settings.general.dbpath = lib.mkDefault "/var/lib/isso/comments.db";
|
||||
|
||||
systemd.services.isso = {
|
||||
description = "isso, a commenting server similar to Disqus";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
User = "isso";
|
||||
Group = "isso";
|
||||
|
||||
DynamicUser = true;
|
||||
|
||||
StateDirectory = "isso";
|
||||
|
||||
ExecStart = ''
|
||||
${pkgs.isso}/bin/isso -c ${configFile}
|
||||
'';
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
173
nixos/modules/services/web-apps/jirafeau.nix
Normal file
173
nixos/modules/services/web-apps/jirafeau.nix
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.jirafeau;
|
||||
|
||||
group = config.services.nginx.group;
|
||||
user = config.services.nginx.user;
|
||||
|
||||
withTrailingSlash = str: if hasSuffix "/" str then str else "${str}/";
|
||||
|
||||
localConfig = pkgs.writeText "config.local.php" ''
|
||||
<?php
|
||||
$cfg['admin_password'] = '${cfg.adminPasswordSha256}';
|
||||
$cfg['web_root'] = 'http://${withTrailingSlash cfg.hostName}';
|
||||
$cfg['var_root'] = '${withTrailingSlash cfg.dataDir}';
|
||||
$cfg['maximal_upload_size'] = ${builtins.toString cfg.maxUploadSizeMegabytes};
|
||||
$cfg['installation_done'] = true;
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.jirafeau = {
|
||||
adminPasswordSha256 = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
SHA-256 of the desired administration password. Leave blank/unset for no password.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/jirafeau/data/";
|
||||
description = "Location of Jirafeau storage directory.";
|
||||
};
|
||||
|
||||
enable = mkEnableOption "Jirafeau file upload application.";
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
example = ''
|
||||
$cfg['style'] = 'courgette';
|
||||
$cfg['organisation'] = 'ACME';
|
||||
'';
|
||||
description = let
|
||||
documentationLink =
|
||||
"https://gitlab.com/mojo42/Jirafeau/-/blob/${cfg.package.version}/lib/config.original.php";
|
||||
in
|
||||
''
|
||||
Jirefeau configuration. Refer to <link xlink:href="${documentationLink}"/> for supported
|
||||
values.
|
||||
'';
|
||||
};
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "URL of instance. Must have trailing slash.";
|
||||
};
|
||||
|
||||
maxUploadSizeMegabytes = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = "Maximum upload size of accepted files.";
|
||||
};
|
||||
|
||||
maxUploadTimeout = mkOption {
|
||||
type = types.str;
|
||||
default = "30m";
|
||||
description = let
|
||||
nginxCoreDocumentation = "http://nginx.org/en/docs/http/ngx_http_core_module.html";
|
||||
in
|
||||
''
|
||||
Timeout for reading client request bodies and headers. Refer to
|
||||
<link xlink:href="${nginxCoreDocumentation}#client_body_timeout"/> and
|
||||
<link xlink:href="${nginxCoreDocumentation}#client_header_timeout"/> for accepted values.
|
||||
'';
|
||||
};
|
||||
|
||||
nginxConfig = mkOption {
|
||||
type = types.submodule
|
||||
(import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
serverAliases = [ "wiki.''${config.networking.domain}" ];
|
||||
}
|
||||
'';
|
||||
description = "Extra configuration for the nginx virtual host of Jirafeau.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jirafeau;
|
||||
defaultText = literalExpression "pkgs.jirafeau";
|
||||
description = "Jirafeau package to use";
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for Jirafeau PHP pool. See documentation on <literal>php-fpm.conf</literal> for
|
||||
details on configuration directives.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services = {
|
||||
nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.hostName}" = mkMerge [
|
||||
cfg.nginxConfig
|
||||
{
|
||||
extraConfig = let
|
||||
clientMaxBodySize =
|
||||
if cfg.maxUploadSizeMegabytes == 0 then "0" else "${cfg.maxUploadSizeMegabytes}m";
|
||||
in
|
||||
''
|
||||
index index.php;
|
||||
client_max_body_size ${clientMaxBodySize};
|
||||
client_body_timeout ${cfg.maxUploadTimeout};
|
||||
client_header_timeout ${cfg.maxUploadTimeout};
|
||||
'';
|
||||
locations = {
|
||||
"~ \\.php$".extraConfig = ''
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.jirafeau.socket};
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
'';
|
||||
};
|
||||
root = mkForce "${cfg.package}";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
phpfpm.pools.jirafeau = {
|
||||
inherit group user;
|
||||
phpEnv."JIRAFEAU_CONFIG" = "${localConfig}";
|
||||
settings = {
|
||||
"listen.mode" = "0660";
|
||||
"listen.owner" = user;
|
||||
"listen.group" = group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/files/ 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/links/ 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/async/ 0750 ${user} ${group} - -"
|
||||
];
|
||||
};
|
||||
|
||||
# uses attributes of the linked package
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
452
nixos/modules/services/web-apps/jitsi-meet.nix
Normal file
452
nixos/modules/services/web-apps/jitsi-meet.nix
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.jitsi-meet;
|
||||
|
||||
# The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to
|
||||
# override only some settings, we need to extract the JSON, use jq to merge it with
|
||||
# the config provided by user, and then reconstruct the file.
|
||||
overrideJs =
|
||||
source: varName: userCfg: appendExtra:
|
||||
let
|
||||
extractor = pkgs.writeText "extractor.js" ''
|
||||
var fs = require("fs");
|
||||
eval(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
process.stdout.write(JSON.stringify(eval(process.argv[3])));
|
||||
'';
|
||||
userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg);
|
||||
in (pkgs.runCommand "${varName}.js" { } ''
|
||||
${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json
|
||||
(
|
||||
echo "var ${varName} = "
|
||||
${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson}
|
||||
echo ";"
|
||||
echo ${escapeShellArg appendExtra}
|
||||
) > $out
|
||||
'');
|
||||
|
||||
# Essential config - it's probably not good to have these as option default because
|
||||
# types.attrs doesn't do merging. Let's merge explicitly, can still be overriden if
|
||||
# user desires.
|
||||
defaultCfg = {
|
||||
hosts = {
|
||||
domain = cfg.hostName;
|
||||
muc = "conference.${cfg.hostName}";
|
||||
focus = "focus.${cfg.hostName}";
|
||||
};
|
||||
bosh = "//${cfg.hostName}/http-bind";
|
||||
websocket = "wss://${cfg.hostName}/xmpp-websocket";
|
||||
|
||||
fileRecordingsEnabled = true;
|
||||
liveStreamingEnabled = true;
|
||||
hiddenDomain = "recorder.${cfg.hostName}";
|
||||
};
|
||||
in
|
||||
{
|
||||
options.services.jitsi-meet = with types; {
|
||||
enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
|
||||
|
||||
hostName = mkOption {
|
||||
type = str;
|
||||
example = "meet.example.org";
|
||||
description = ''
|
||||
FQDN of the Jitsi Meet instance.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = attrs;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
enableWelcomePage = false;
|
||||
defaultLang = "fi";
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Client-side web application settings that override the defaults in <filename>config.js</filename>.
|
||||
|
||||
See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/config.js" /> for default
|
||||
configuration with comments.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Text to append to <filename>config.js</filename> web application config file.
|
||||
|
||||
Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
|
||||
'';
|
||||
};
|
||||
|
||||
interfaceConfig = mkOption {
|
||||
type = attrs;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
SHOW_JITSI_WATERMARK = false;
|
||||
SHOW_WATERMARK_FOR_GUESTS = false;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
|
||||
|
||||
See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js" /> for
|
||||
default configuration with comments.
|
||||
'';
|
||||
};
|
||||
|
||||
videobridge = {
|
||||
enable = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
|
||||
|
||||
Additional configuration is possible with <option>services.jitsi-videobridge</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
example = "/run/keys/videobridge";
|
||||
description = ''
|
||||
File containing password to the Prosody account for videobridge.
|
||||
|
||||
If <literal>null</literal>, a file with password will be generated automatically. Setting
|
||||
this option is useful if you plan to connect additional videobridges to the XMPP server.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
jicofo.enable = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to enable JiCoFo instance and configure it to connect to Prosody.
|
||||
|
||||
Additional configuration is possible with <option>services.jicofo</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
jibri.enable = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable a Jibri instance and configure it to connect to Prosody.
|
||||
|
||||
Additional configuration is possible with <option>services.jibri</option>, and
|
||||
<option>services.jibri.finalizeScript</option> is especially useful.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx.enable = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to enable nginx virtual host that will serve the javascript application and act as
|
||||
a proxy for the XMPP server. Further nginx configuration can be done by adapting
|
||||
<option>services.nginx.virtualHosts.<hostName></option>.
|
||||
When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
|
||||
this, set the <option>services.nginx.virtualHosts.<hostName>.enableACME</option> to
|
||||
<literal>false</literal> and if appropriate do the same for
|
||||
<option>services.nginx.virtualHosts.<hostName>.forceSSL</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
caddy.enable = mkEnableOption "Whether to enable caddy reverse proxy to expose jitsi-meet";
|
||||
|
||||
prosody.enable = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
|
||||
off if you want to configure it manually.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.prosody = mkIf cfg.prosody.enable {
|
||||
enable = mkDefault true;
|
||||
xmppComplianceSuite = mkDefault false;
|
||||
modules = {
|
||||
admin_adhoc = mkDefault false;
|
||||
bosh = mkDefault true;
|
||||
ping = mkDefault true;
|
||||
roster = mkDefault true;
|
||||
saslauth = mkDefault true;
|
||||
smacks = mkDefault true;
|
||||
tls = mkDefault true;
|
||||
websocket = mkDefault true;
|
||||
};
|
||||
muc = [
|
||||
{
|
||||
domain = "conference.${cfg.hostName}";
|
||||
name = "Jitsi Meet MUC";
|
||||
roomLocking = false;
|
||||
roomDefaultPublicJids = true;
|
||||
extraConfig = ''
|
||||
storage = "memory"
|
||||
'';
|
||||
}
|
||||
{
|
||||
domain = "internal.${cfg.hostName}";
|
||||
name = "Jitsi Meet Videobridge MUC";
|
||||
extraConfig = ''
|
||||
storage = "memory"
|
||||
admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" }
|
||||
'';
|
||||
#-- muc_room_cache_size = 1000
|
||||
}
|
||||
];
|
||||
extraModules = [ "pubsub" "smacks" ];
|
||||
extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
|
||||
extraConfig = lib.mkMerge [ (mkAfter ''
|
||||
Component "focus.${cfg.hostName}" "client_proxy"
|
||||
target_address = "focus@auth.${cfg.hostName}"
|
||||
'')
|
||||
(mkBefore ''
|
||||
cross_domain_websocket = true;
|
||||
consider_websocket_secure = true;
|
||||
'')
|
||||
];
|
||||
virtualHosts.${cfg.hostName} = {
|
||||
enabled = true;
|
||||
domain = cfg.hostName;
|
||||
extraConfig = ''
|
||||
authentication = "anonymous"
|
||||
c2s_require_encryption = false
|
||||
admins = { "focus@auth.${cfg.hostName}" }
|
||||
smacks_max_unacked_stanzas = 5
|
||||
smacks_hibernation_time = 60
|
||||
smacks_max_hibernated_sessions = 1
|
||||
smacks_max_old_sessions = 1
|
||||
'';
|
||||
ssl = {
|
||||
cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
|
||||
key = "/var/lib/jitsi-meet/jitsi-meet.key";
|
||||
};
|
||||
};
|
||||
virtualHosts."auth.${cfg.hostName}" = {
|
||||
enabled = true;
|
||||
domain = "auth.${cfg.hostName}";
|
||||
extraConfig = ''
|
||||
authentication = "internal_plain"
|
||||
'';
|
||||
ssl = {
|
||||
cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
|
||||
key = "/var/lib/jitsi-meet/jitsi-meet.key";
|
||||
};
|
||||
};
|
||||
virtualHosts."recorder.${cfg.hostName}" = {
|
||||
enabled = true;
|
||||
domain = "recorder.${cfg.hostName}";
|
||||
extraConfig = ''
|
||||
authentication = "internal_plain"
|
||||
c2s_require_encryption = false
|
||||
'';
|
||||
};
|
||||
};
|
||||
systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
|
||||
EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
|
||||
SupplementaryGroups = [ "jitsi-meet" ];
|
||||
};
|
||||
|
||||
users.groups.jitsi-meet = {};
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
|
||||
];
|
||||
|
||||
systemd.services.jitsi-meet-init-secrets = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
|
||||
path = [ config.services.prosody.package ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
};
|
||||
|
||||
script = let
|
||||
secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
|
||||
videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
|
||||
in
|
||||
''
|
||||
cd /var/lib/jitsi-meet
|
||||
${concatMapStringsSep "\n" (s: ''
|
||||
if [ ! -f ${s} ]; then
|
||||
tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
|
||||
chown root:jitsi-meet ${s}
|
||||
chmod 640 ${s}
|
||||
fi
|
||||
'') secrets}
|
||||
|
||||
# for easy access in prosody
|
||||
echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
|
||||
chown root:jitsi-meet secrets-env
|
||||
chmod 640 secrets-env
|
||||
''
|
||||
+ optionalString cfg.prosody.enable ''
|
||||
prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
|
||||
prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
|
||||
prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
|
||||
prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
|
||||
prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
|
||||
|
||||
# generate self-signed certificates
|
||||
if [ ! -f /var/lib/jitsi-meet.crt ]; then
|
||||
${getBin pkgs.openssl}/bin/openssl req \
|
||||
-x509 \
|
||||
-newkey rsa:4096 \
|
||||
-keyout /var/lib/jitsi-meet/jitsi-meet.key \
|
||||
-out /var/lib/jitsi-meet/jitsi-meet.crt \
|
||||
-days 36500 \
|
||||
-nodes \
|
||||
-subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
|
||||
chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key}
|
||||
chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key}
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
services.nginx = mkIf cfg.nginx.enable {
|
||||
enable = mkDefault true;
|
||||
virtualHosts.${cfg.hostName} = {
|
||||
enableACME = mkDefault true;
|
||||
forceSSL = mkDefault true;
|
||||
root = pkgs.jitsi-meet;
|
||||
extraConfig = ''
|
||||
ssi on;
|
||||
'';
|
||||
locations."@root_path".extraConfig = ''
|
||||
rewrite ^/(.*)$ / break;
|
||||
'';
|
||||
locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
|
||||
locations."^~ /xmpp-websocket" = {
|
||||
priority = 100;
|
||||
proxyPass = "http://localhost:5280/xmpp-websocket";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
locations."=/http-bind" = {
|
||||
proxyPass = "http://localhost:5280/http-bind";
|
||||
extraConfig = ''
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
'';
|
||||
};
|
||||
locations."=/external_api.js" = mkDefault {
|
||||
alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
|
||||
};
|
||||
locations."=/config.js" = mkDefault {
|
||||
alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
|
||||
};
|
||||
locations."=/interface_config.js" = mkDefault {
|
||||
alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.caddy = mkIf cfg.caddy.enable {
|
||||
enable = mkDefault true;
|
||||
virtualHosts.${cfg.hostName} = {
|
||||
extraConfig =
|
||||
let
|
||||
templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" {} ''
|
||||
cp -R ${pkgs.jitsi-meet}/* .
|
||||
for file in *.html **/*.html ; do
|
||||
${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
|
||||
done
|
||||
rm config.js
|
||||
rm interface_config.js
|
||||
cp -R . $out
|
||||
cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
|
||||
cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
|
||||
cp ./libs/external_api.min.js $out/external_api.js
|
||||
'';
|
||||
in ''
|
||||
handle /http-bind {
|
||||
header Host ${cfg.hostName}
|
||||
reverse_proxy 127.0.0.1:5280
|
||||
}
|
||||
handle /xmpp-websocket {
|
||||
reverse_proxy 127.0.0.1:5280
|
||||
}
|
||||
handle {
|
||||
templates
|
||||
root * ${templatedJitsiMeet}
|
||||
try_files {path} {path}
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
services.jitsi-videobridge = mkIf cfg.videobridge.enable {
|
||||
enable = true;
|
||||
xmppConfigs."localhost" = {
|
||||
userName = "jvb";
|
||||
domain = "auth.${cfg.hostName}";
|
||||
passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
|
||||
mucJids = "jvbbrewery@internal.${cfg.hostName}";
|
||||
disableCertificateVerification = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.jicofo = mkIf cfg.jicofo.enable {
|
||||
enable = true;
|
||||
xmppHost = "localhost";
|
||||
xmppDomain = cfg.hostName;
|
||||
userDomain = "auth.${cfg.hostName}";
|
||||
userName = "focus";
|
||||
userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
|
||||
componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
|
||||
bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
|
||||
config = mkMerge [{
|
||||
"org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
|
||||
#} (lib.mkIf cfg.jibri.enable {
|
||||
} (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
|
||||
"org.jitsi.jicofo.jibri.BREWERY" = "JibriBrewery@internal.${cfg.hostName}";
|
||||
"org.jitsi.jicofo.jibri.PENDING_TIMEOUT" = "90";
|
||||
})];
|
||||
};
|
||||
|
||||
services.jibri = mkIf cfg.jibri.enable {
|
||||
enable = true;
|
||||
|
||||
xmppEnvironments."jitsi-meet" = {
|
||||
xmppServerHosts = [ "localhost" ];
|
||||
xmppDomain = cfg.hostName;
|
||||
|
||||
control.muc = {
|
||||
domain = "internal.${cfg.hostName}";
|
||||
roomName = "JibriBrewery";
|
||||
nickname = "jibri";
|
||||
};
|
||||
|
||||
control.login = {
|
||||
domain = "auth.${cfg.hostName}";
|
||||
username = "jibri";
|
||||
passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
|
||||
};
|
||||
|
||||
call.login = {
|
||||
domain = "recorder.${cfg.hostName}";
|
||||
username = "recorder";
|
||||
passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
|
||||
};
|
||||
|
||||
usageTimeout = "0";
|
||||
disableCertificateVerification = true;
|
||||
stripFromRoomDomain = "conference.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.doc = ./jitsi-meet.xml;
|
||||
meta.maintainers = lib.teams.jitsi.members;
|
||||
}
|
||||
55
nixos/modules/services/web-apps/jitsi-meet.xml
Normal file
55
nixos/modules/services/web-apps/jitsi-meet.xml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-jitsi-meet">
|
||||
<title>Jitsi Meet</title>
|
||||
<para>
|
||||
With Jitsi Meet on NixOS you can quickly configure a complete,
|
||||
private, self-hosted video conferencing solution.
|
||||
</para>
|
||||
|
||||
<section xml:id="module-services-jitsi-basic-usage">
|
||||
<title>Basic usage</title>
|
||||
<para>
|
||||
A minimal configuration using Let's Encrypt for TLS certificates looks like this:
|
||||
<programlisting>{
|
||||
services.jitsi-meet = {
|
||||
<link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
|
||||
<link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
|
||||
};
|
||||
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
||||
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||
}</programlisting>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-jitsi-configuration">
|
||||
<title>Configuration</title>
|
||||
<para>
|
||||
Here is the minimal configuration with additional configurations:
|
||||
<programlisting>{
|
||||
services.jitsi-meet = {
|
||||
<link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
|
||||
<link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
|
||||
<link linkend="opt-services.jitsi-meet.config">config</link> = {
|
||||
enableWelcomePage = false;
|
||||
prejoinPageEnabled = true;
|
||||
defaultLang = "fi";
|
||||
};
|
||||
<link linkend="opt-services.jitsi-meet.interfaceConfig">interfaceConfig</link> = {
|
||||
SHOW_JITSI_WATERMARK = false;
|
||||
SHOW_WATERMARK_FOR_GUESTS = false;
|
||||
};
|
||||
};
|
||||
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
||||
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||
}</programlisting>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
</chapter>
|
||||
683
nixos/modules/services/web-apps/keycloak.nix
Normal file
683
nixos/modules/services/web-apps/keycloak.nix
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
{ config, options, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.keycloak;
|
||||
opt = options.services.keycloak;
|
||||
|
||||
inherit (lib)
|
||||
types
|
||||
mkMerge
|
||||
mkOption
|
||||
mkChangedOptionModule
|
||||
mkRenamedOptionModule
|
||||
mkRemovedOptionModule
|
||||
concatStringsSep
|
||||
mapAttrsToList
|
||||
escapeShellArg
|
||||
mkIf
|
||||
optionalString
|
||||
optionals
|
||||
mkDefault
|
||||
literalExpression
|
||||
isAttrs
|
||||
literalDocBook
|
||||
maintainers
|
||||
catAttrs
|
||||
collect
|
||||
splitString
|
||||
;
|
||||
|
||||
inherit (builtins)
|
||||
elem
|
||||
typeOf
|
||||
isInt
|
||||
isString
|
||||
hashString
|
||||
isPath
|
||||
;
|
||||
|
||||
prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
|
||||
in
|
||||
{
|
||||
imports =
|
||||
[
|
||||
(mkRenamedOptionModule
|
||||
[ "services" "keycloak" "bindAddress" ]
|
||||
[ "services" "keycloak" "settings" "http-host" ])
|
||||
(mkRenamedOptionModule
|
||||
[ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
|
||||
[ "services" "keycloak" "settings" "hostname-strict-backchannel"])
|
||||
(mkChangedOptionModule
|
||||
[ "services" "keycloak" "httpPort" ]
|
||||
[ "services" "keycloak" "settings" "http-port" ]
|
||||
(config:
|
||||
builtins.fromJSON config.services.keycloak.httpPort))
|
||||
(mkChangedOptionModule
|
||||
[ "services" "keycloak" "httpsPort" ]
|
||||
[ "services" "keycloak" "settings" "https-port" ]
|
||||
(config:
|
||||
builtins.fromJSON config.services.keycloak.httpsPort))
|
||||
(mkRemovedOptionModule
|
||||
[ "services" "keycloak" "frontendUrl" ]
|
||||
''
|
||||
Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
|
||||
NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
|
||||
See its description for more information.
|
||||
'')
|
||||
(mkRemovedOptionModule
|
||||
[ "services" "keycloak" "extraConfig" ]
|
||||
"Use `services.keycloak.settings' instead.")
|
||||
];
|
||||
|
||||
options.services.keycloak =
|
||||
let
|
||||
inherit (types)
|
||||
bool
|
||||
str
|
||||
int
|
||||
nullOr
|
||||
attrsOf
|
||||
oneOf
|
||||
path
|
||||
enum
|
||||
package
|
||||
port;
|
||||
|
||||
assertStringPath = optionName: value:
|
||||
if isPath value then
|
||||
throw ''
|
||||
services.keycloak.${optionName}:
|
||||
${toString value}
|
||||
is a Nix path, but should be a string, since Nix
|
||||
paths are copied into the world-readable Nix store.
|
||||
''
|
||||
else value;
|
||||
in
|
||||
{
|
||||
enable = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether to enable the Keycloak identity and access management
|
||||
server.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCertificate = mkOption {
|
||||
type = nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/ssl_cert";
|
||||
apply = assertStringPath "sslCertificate";
|
||||
description = ''
|
||||
The path to a PEM formatted certificate to use for TLS/SSL
|
||||
connections.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCertificateKey = mkOption {
|
||||
type = nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/ssl_key";
|
||||
apply = assertStringPath "sslCertificateKey";
|
||||
description = ''
|
||||
The path to a PEM formatted private key to use for TLS/SSL
|
||||
connections.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Keycloak plugin jar, ear files or derivations containing
|
||||
them. Packaged plugins are available through
|
||||
<literal>pkgs.keycloak.plugins</literal>.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = enum [ "mysql" "mariadb" "postgresql" ];
|
||||
default = "postgresql";
|
||||
example = "mariadb";
|
||||
description = ''
|
||||
The type of database Keycloak should connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Hostname of the database to connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
port =
|
||||
let
|
||||
dbPorts = {
|
||||
postgresql = 5432;
|
||||
mariadb = 3306;
|
||||
mysql = 3306;
|
||||
};
|
||||
in
|
||||
mkOption {
|
||||
type = port;
|
||||
default = dbPorts.${cfg.database.type};
|
||||
defaultText = literalDocBook "default port of selected database";
|
||||
description = ''
|
||||
Port of the database to connect to.
|
||||
'';
|
||||
};
|
||||
|
||||
useSSL = mkOption {
|
||||
type = bool;
|
||||
default = cfg.database.host != "localhost";
|
||||
defaultText = literalExpression ''config.${opt.database.host} != "localhost"'';
|
||||
description = ''
|
||||
Whether the database connection should be secured by SSL /
|
||||
TLS.
|
||||
'';
|
||||
};
|
||||
|
||||
caCert = mkOption {
|
||||
type = nullOr path;
|
||||
default = null;
|
||||
description = ''
|
||||
The SSL / TLS CA certificate that verifies the identity of the
|
||||
database server.
|
||||
|
||||
Required when PostgreSQL is used and SSL is turned on.
|
||||
|
||||
For MySQL, if left at <literal>null</literal>, the default
|
||||
Java keystore is used, which should suffice if the server
|
||||
certificate is issued by an official CA.
|
||||
'';
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether a database should be automatically created on the
|
||||
local host. Set this to false if you plan on provisioning a
|
||||
local database yourself. This has no effect if
|
||||
services.keycloak.database.host is customized.
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = str;
|
||||
default = "keycloak";
|
||||
description = ''
|
||||
Database name to use when connecting to an external or
|
||||
manually provisioned database; has no effect when a local
|
||||
database is automatically provisioned.
|
||||
|
||||
To use this with a local database, set <xref
|
||||
linkend="opt-services.keycloak.database.createLocally" /> to
|
||||
<literal>false</literal> and create the database and user
|
||||
manually.
|
||||
'';
|
||||
};
|
||||
|
||||
username = mkOption {
|
||||
type = str;
|
||||
default = "keycloak";
|
||||
description = ''
|
||||
Username to use when connecting to an external or manually
|
||||
provisioned database; has no effect when a local database is
|
||||
automatically provisioned.
|
||||
|
||||
To use this with a local database, set <xref
|
||||
linkend="opt-services.keycloak.database.createLocally" /> to
|
||||
<literal>false</literal> and create the database and user
|
||||
manually.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = path;
|
||||
example = "/run/keys/db_password";
|
||||
apply = assertStringPath "passwordFile";
|
||||
description = ''
|
||||
The path to a file containing the database password.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = package;
|
||||
default = pkgs.keycloak;
|
||||
defaultText = literalExpression "pkgs.keycloak";
|
||||
description = ''
|
||||
Keycloak package to use.
|
||||
'';
|
||||
};
|
||||
|
||||
initialAdminPassword = mkOption {
|
||||
type = str;
|
||||
default = "changeme";
|
||||
description = ''
|
||||
Initial password set for the <literal>admin</literal>
|
||||
user. The password is not stored safely and should be changed
|
||||
immediately in the admin panel.
|
||||
'';
|
||||
};
|
||||
|
||||
themes = mkOption {
|
||||
type = attrsOf package;
|
||||
default = { };
|
||||
description = ''
|
||||
Additional theme packages for Keycloak. Each theme is linked into
|
||||
subdirectory with a corresponding attribute name.
|
||||
|
||||
Theme packages consist of several subdirectories which provide
|
||||
different theme types: for example, <literal>account</literal>,
|
||||
<literal>login</literal> etc. After adding a theme to this option you
|
||||
can select it by its name in Keycloak administration console.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
|
||||
|
||||
options = {
|
||||
http-host = mkOption {
|
||||
type = str;
|
||||
default = "0.0.0.0";
|
||||
example = "127.0.0.1";
|
||||
description = ''
|
||||
On which address Keycloak should accept new connections.
|
||||
'';
|
||||
};
|
||||
|
||||
http-port = mkOption {
|
||||
type = port;
|
||||
default = 80;
|
||||
example = 8080;
|
||||
description = ''
|
||||
On which port Keycloak should listen for new HTTP connections.
|
||||
'';
|
||||
};
|
||||
|
||||
https-port = mkOption {
|
||||
type = port;
|
||||
default = 443;
|
||||
example = 8443;
|
||||
description = ''
|
||||
On which port Keycloak should listen for new HTTPS connections.
|
||||
'';
|
||||
};
|
||||
|
||||
http-relative-path = mkOption {
|
||||
type = str;
|
||||
default = "";
|
||||
example = "/auth";
|
||||
description = ''
|
||||
The path relative to <literal>/</literal> for serving
|
||||
resources.
|
||||
|
||||
<note>
|
||||
<para>
|
||||
In versions of Keycloak using Wildfly (<17),
|
||||
this defaulted to <literal>/auth</literal>. If
|
||||
upgrading from the Wildfly version of Keycloak,
|
||||
i.e. a NixOS version before 22.05, you'll likely
|
||||
want to set this to <literal>/auth</literal> to
|
||||
keep compatibility with your clients.
|
||||
|
||||
See <link
|
||||
xlink:href="https://www.keycloak.org/migration/migrating-to-quarkus"
|
||||
/> for more information on migrating from Wildfly
|
||||
to Quarkus.
|
||||
</para>
|
||||
</note>
|
||||
'';
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = str;
|
||||
example = "keycloak.example.com";
|
||||
description = ''
|
||||
The hostname part of the public URL used as base for
|
||||
all frontend requests.
|
||||
|
||||
See <link xlink:href="https://www.keycloak.org/server/hostname" />
|
||||
for more information about hostname configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
hostname-strict-backchannel = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether Keycloak should force all requests to go
|
||||
through the frontend URL. By default, Keycloak allows
|
||||
backend requests to instead use its local hostname or
|
||||
IP address and may also advertise it to clients
|
||||
through its OpenID Connect Discovery endpoint.
|
||||
|
||||
See <link xlink:href="https://www.keycloak.org/server/hostname" />
|
||||
for more information about hostname configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
proxy = mkOption {
|
||||
type = enum [ "edge" "reencrypt" "passthrough" "none" ];
|
||||
default = "none";
|
||||
example = "edge";
|
||||
description = ''
|
||||
The proxy address forwarding mode if the server is
|
||||
behind a reverse proxy.
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term>edge</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Enables communication through HTTP between the
|
||||
proxy and Keycloak.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>reencrypt</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Requires communication through HTTPS between the
|
||||
proxy and Keycloak.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>passthrough</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Enables communication through HTTP or HTTPS between
|
||||
the proxy and Keycloak.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
See <link
|
||||
xlink:href="https://www.keycloak.org/server/reverseproxy"
|
||||
/> for more information.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostname = "keycloak.example.com";
|
||||
proxy = "reencrypt";
|
||||
https-key-store-file = "/path/to/file";
|
||||
https-key-store-password = { _secret = "/run/keys/store_password"; };
|
||||
}
|
||||
'';
|
||||
|
||||
description = ''
|
||||
Configuration options corresponding to parameters set in
|
||||
<filename>conf/keycloak.conf</filename>.
|
||||
|
||||
Most available options are documented at <link
|
||||
xlink:href="https://www.keycloak.org/server/all-config" />.
|
||||
|
||||
Options containing secret data should be set to an attribute
|
||||
set containing the attribute <literal>_secret</literal> - a
|
||||
string pointing to a file containing the value the option
|
||||
should be set to. See the example to get a better picture of
|
||||
this: in the resulting
|
||||
<filename>conf/keycloak.conf</filename> file, the
|
||||
<literal>https-key-store-password</literal> key will be set
|
||||
to the contents of the
|
||||
<filename>/run/keys/store_password</filename> file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
# We only want to create a database if we're actually going to
|
||||
# connect to it.
|
||||
databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
|
||||
createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
|
||||
createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
|
||||
|
||||
mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
|
||||
${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
|
||||
'';
|
||||
|
||||
# Both theme and theme type directories need to be actual
|
||||
# directories in one hierarchy to pass Keycloak checks.
|
||||
themesBundle = pkgs.runCommand "keycloak-themes" { } ''
|
||||
linkTheme() {
|
||||
theme="$1"
|
||||
name="$2"
|
||||
|
||||
mkdir "$out/$name"
|
||||
for typeDir in "$theme"/*; do
|
||||
if [ -d "$typeDir" ]; then
|
||||
type="$(basename "$typeDir")"
|
||||
mkdir "$out/$name/$type"
|
||||
for file in "$typeDir"/*; do
|
||||
ln -sn "$file" "$out/$name/$type/$(basename "$file")"
|
||||
done
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p "$out"
|
||||
for theme in ${keycloakBuild}/themes/*; do
|
||||
if [ -d "$theme" ]; then
|
||||
linkTheme "$theme" "$(basename "$theme")"
|
||||
fi
|
||||
done
|
||||
|
||||
${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
|
||||
'';
|
||||
|
||||
keycloakConfig = lib.generators.toKeyValue {
|
||||
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
||||
mkValueString = v: with builtins;
|
||||
if isInt v then toString v
|
||||
else if isString v then v
|
||||
else if true == v then "true"
|
||||
else if false == v then "false"
|
||||
else if isSecret v then hashString "sha256" v._secret
|
||||
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
||||
};
|
||||
};
|
||||
|
||||
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
|
||||
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
|
||||
confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
|
||||
keycloakBuild = cfg.package.override {
|
||||
inherit confFile;
|
||||
plugins = cfg.package.enabledPlugins ++ cfg.plugins;
|
||||
};
|
||||
in
|
||||
mkIf cfg.enable
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
|
||||
message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = [ keycloakBuild ];
|
||||
|
||||
services.keycloak.settings =
|
||||
let
|
||||
postgresParams = concatStringsSep "&" (
|
||||
optionals cfg.database.useSSL [
|
||||
"ssl=true"
|
||||
] ++ optionals (cfg.database.caCert != null) [
|
||||
"sslrootcert=${cfg.database.caCert}"
|
||||
"sslmode=verify-ca"
|
||||
]
|
||||
);
|
||||
mariadbParams = concatStringsSep "&" ([
|
||||
"characterEncoding=UTF-8"
|
||||
] ++ optionals cfg.database.useSSL [
|
||||
"useSSL=true"
|
||||
"requireSSL=true"
|
||||
"verifyServerCertificate=true"
|
||||
] ++ optionals (cfg.database.caCert != null) [
|
||||
"trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
|
||||
"trustCertificateKeyStorePassword=notsosecretpassword"
|
||||
]);
|
||||
dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
|
||||
in
|
||||
mkMerge [
|
||||
{
|
||||
db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
|
||||
db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
|
||||
db-password._secret = cfg.database.passwordFile;
|
||||
db-url-host = cfg.database.host;
|
||||
db-url-port = toString cfg.database.port;
|
||||
db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
|
||||
db-url-properties = prefixUnlessEmpty "?" dbProps;
|
||||
db-url = null;
|
||||
}
|
||||
(mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
|
||||
https-certificate-file = "/run/keycloak/ssl/ssl_cert";
|
||||
https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
|
||||
})
|
||||
];
|
||||
|
||||
systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
|
||||
after = [ "postgresql.service" ];
|
||||
before = [ "keycloak.service" ];
|
||||
bindsTo = [ "postgresql.service" ];
|
||||
path = [ config.services.postgresql.package ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = "postgres";
|
||||
Group = "postgres";
|
||||
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
|
||||
};
|
||||
script = ''
|
||||
set -o errexit -o pipefail -o nounset -o errtrace
|
||||
shopt -s inherit_errexit
|
||||
|
||||
create_role="$(mktemp)"
|
||||
trap 'rm -f "$create_role"' ERR EXIT
|
||||
|
||||
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
|
||||
echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role"
|
||||
psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
|
||||
psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.keycloakMySQLInit = mkIf createLocalMySQL {
|
||||
after = [ "mysql.service" ];
|
||||
before = [ "keycloak.service" ];
|
||||
bindsTo = [ "mysql.service" ];
|
||||
path = [ config.services.mysql.package ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = config.services.mysql.user;
|
||||
Group = config.services.mysql.group;
|
||||
LoadCredential = [ "db_password:${cfg.database.passwordFile}" ];
|
||||
};
|
||||
script = ''
|
||||
set -o errexit -o pipefail -o nounset -o errtrace
|
||||
shopt -s inherit_errexit
|
||||
db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")"
|
||||
( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
|
||||
echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
|
||||
echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
|
||||
) | mysql -N
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.keycloak =
|
||||
let
|
||||
databaseServices =
|
||||
if createLocalPostgreSQL then [
|
||||
"keycloakPostgreSQLInit.service"
|
||||
"postgresql.service"
|
||||
]
|
||||
else if createLocalMySQL then [
|
||||
"keycloakMySQLInit.service"
|
||||
"mysql.service"
|
||||
]
|
||||
else [ ];
|
||||
secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
|
||||
mkSecretReplacement = file: ''
|
||||
replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
|
||||
'';
|
||||
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
||||
in
|
||||
{
|
||||
after = databaseServices;
|
||||
bindsTo = databaseServices;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = with pkgs; [
|
||||
keycloakBuild
|
||||
openssl
|
||||
replace-secret
|
||||
];
|
||||
environment = {
|
||||
KC_HOME_DIR = "/run/keycloak";
|
||||
KC_CONF_DIR = "/run/keycloak/conf";
|
||||
};
|
||||
serviceConfig = {
|
||||
LoadCredential =
|
||||
map (p: "${baseNameOf p}:${p}") secretPaths
|
||||
++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
|
||||
"ssl_cert:${cfg.sslCertificate}"
|
||||
"ssl_key:${cfg.sslCertificateKey}"
|
||||
];
|
||||
User = "keycloak";
|
||||
Group = "keycloak";
|
||||
DynamicUser = true;
|
||||
RuntimeDirectory = "keycloak";
|
||||
RuntimeDirectoryMode = 0700;
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
};
|
||||
script = ''
|
||||
set -o errexit -o pipefail -o nounset -o errtrace
|
||||
shopt -s inherit_errexit
|
||||
|
||||
umask u=rwx,g=,o=
|
||||
|
||||
ln -s ${themesBundle} /run/keycloak/themes
|
||||
ln -s ${keycloakBuild}/providers /run/keycloak/
|
||||
|
||||
install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
|
||||
|
||||
${secretReplacements}
|
||||
|
||||
'' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
|
||||
mkdir -p /run/keycloak/ssl
|
||||
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
|
||||
'' + ''
|
||||
export KEYCLOAK_ADMIN=admin
|
||||
export KEYCLOAK_ADMIN_PASSWORD=${cfg.initialAdminPassword}
|
||||
kc.sh start
|
||||
'';
|
||||
};
|
||||
|
||||
services.postgresql.enable = mkDefault createLocalPostgreSQL;
|
||||
services.mysql.enable = mkDefault createLocalMySQL;
|
||||
services.mysql.package =
|
||||
let
|
||||
dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
|
||||
in
|
||||
mkIf createLocalMySQL (mkDefault dbPkg);
|
||||
};
|
||||
|
||||
meta.doc = ./keycloak.xml;
|
||||
meta.maintainers = [ maintainers.talyz ];
|
||||
}
|
||||
202
nixos/modules/services/web-apps/keycloak.xml
Normal file
202
nixos/modules/services/web-apps/keycloak.xml
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-keycloak">
|
||||
<title>Keycloak</title>
|
||||
<para>
|
||||
<link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
|
||||
open source identity and access management server with support for
|
||||
<link xlink:href="https://openid.net/connect/">OpenID
|
||||
Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
|
||||
2.0</link> and <link
|
||||
xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
|
||||
2.0</link>.
|
||||
</para>
|
||||
<section xml:id="module-services-keycloak-admin">
|
||||
<title>Administration</title>
|
||||
<para>
|
||||
An administrative user with the username
|
||||
<literal>admin</literal> is automatically created in the
|
||||
<literal>master</literal> realm. Its initial password can be
|
||||
configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
|
||||
and defaults to <literal>changeme</literal>. The password is
|
||||
not stored safely and should be changed immediately in the
|
||||
admin panel.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Refer to the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
|
||||
Keycloak Server Administration Guide</link> for information on
|
||||
how to administer your <productname>Keycloak</productname>
|
||||
instance.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-database">
|
||||
<title>Database access</title>
|
||||
<para>
|
||||
<productname>Keycloak</productname> can be used with either
|
||||
<productname>PostgreSQL</productname>,
|
||||
<productname>MariaDB</productname> or
|
||||
<productname>MySQL</productname>. Which one is used can be
|
||||
configured in <xref
|
||||
linkend="opt-services.keycloak.database.type" />. The selected
|
||||
database will automatically be enabled and a database and role
|
||||
created unless <xref
|
||||
linkend="opt-services.keycloak.database.host" /> is changed
|
||||
from its default of <literal>localhost</literal> or <xref
|
||||
linkend="opt-services.keycloak.database.createLocally" /> is
|
||||
set to <literal>false</literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
External database access can also be configured by setting
|
||||
<xref linkend="opt-services.keycloak.database.host" />, <xref
|
||||
linkend="opt-services.keycloak.database.name" />, <xref
|
||||
linkend="opt-services.keycloak.database.username" />, <xref
|
||||
linkend="opt-services.keycloak.database.useSSL" /> and <xref
|
||||
linkend="opt-services.keycloak.database.caCert" /> as
|
||||
appropriate. Note that you need to manually create the database
|
||||
and allow the configured database user full access to it.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
<xref linkend="opt-services.keycloak.database.passwordFile" />
|
||||
must be set to the path to a file containing the password used
|
||||
to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
|
||||
and <xref linkend="opt-services.keycloak.database.createLocally" />
|
||||
are kept at their defaults, the database role
|
||||
<literal>keycloak</literal> with that password is provisioned
|
||||
on the local database instance.
|
||||
</para>
|
||||
|
||||
<warning>
|
||||
<para>
|
||||
The path should be provided as a string, not a Nix path, since Nix
|
||||
paths are copied into the world readable Nix store.
|
||||
</para>
|
||||
</warning>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-hostname">
|
||||
<title>Hostname</title>
|
||||
<para>
|
||||
The hostname is used to build the public URL used as base for
|
||||
all frontend requests and must be configured through <xref
|
||||
linkend="opt-services.keycloak.settings.hostname" />.
|
||||
</para>
|
||||
|
||||
<note>
|
||||
<para>
|
||||
If you're migrating an old Wildfly based Keycloak instance
|
||||
and want to keep compatibility with your current clients,
|
||||
you'll likely want to set <xref
|
||||
linkend="opt-services.keycloak.settings.http-relative-path"
|
||||
/> to <literal>/auth</literal>. See the option description
|
||||
for more details.
|
||||
</para>
|
||||
</note>
|
||||
|
||||
<para>
|
||||
<xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
|
||||
determines whether Keycloak should force all requests to go
|
||||
through the frontend URL. By default,
|
||||
<productname>Keycloak</productname> allows backend requests to
|
||||
instead use its local hostname or IP address and may also
|
||||
advertise it to clients through its OpenID Connect Discovery
|
||||
endpoint.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
For more information on hostname configuration, see the <link
|
||||
xlink:href="https://www.keycloak.org/server/hostname">Hostname
|
||||
section of the Keycloak Server Installation and Configuration
|
||||
Guide</link>.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-tls">
|
||||
<title>Setting up TLS/SSL</title>
|
||||
<para>
|
||||
By default, <productname>Keycloak</productname> won't accept
|
||||
unsecured HTTP connections originating from outside its local
|
||||
network.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
HTTPS support requires a TLS/SSL certificate and a private key,
|
||||
both <link
|
||||
xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
|
||||
formatted</link>. Their paths should be set through <xref
|
||||
linkend="opt-services.keycloak.sslCertificate" /> and <xref
|
||||
linkend="opt-services.keycloak.sslCertificateKey" />.
|
||||
</para>
|
||||
|
||||
<warning>
|
||||
<para>
|
||||
The paths should be provided as a strings, not a Nix paths,
|
||||
since Nix paths are copied into the world readable Nix store.
|
||||
</para>
|
||||
</warning>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-themes">
|
||||
<title>Themes</title>
|
||||
<para>
|
||||
You can package custom themes and make them visible to
|
||||
Keycloak through <xref linkend="opt-services.keycloak.themes"
|
||||
/>. See the <link
|
||||
xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
|
||||
Themes section of the Keycloak Server Development Guide</link>
|
||||
and the description of the aforementioned NixOS option for
|
||||
more information.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-keycloak-settings">
|
||||
<title>Configuration file settings</title>
|
||||
<para>
|
||||
Keycloak server configuration parameters can be set in <xref
|
||||
linkend="opt-services.keycloak.settings" />. These correspond
|
||||
directly to options in
|
||||
<filename>conf/keycloak.conf</filename>. Some of the most
|
||||
important parameters are documented as suboptions, the rest can
|
||||
be found in the <link
|
||||
xlink:href="https://www.keycloak.org/server/all-config">All
|
||||
configuration section of the Keycloak Server Installation and
|
||||
Configuration Guide</link>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Options containing secret data should be set to an attribute
|
||||
set containing the attribute <literal>_secret</literal> - a
|
||||
string pointing to a file containing the value the option
|
||||
should be set to. See the description of <xref
|
||||
linkend="opt-services.keycloak.settings" /> for an example.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
|
||||
<section xml:id="module-services-keycloak-example-config">
|
||||
<title>Example configuration</title>
|
||||
<para>
|
||||
A basic configuration with some custom settings could look like this:
|
||||
<programlisting>
|
||||
services.keycloak = {
|
||||
<link linkend="opt-services.keycloak.enable">enable</link> = true;
|
||||
settings = {
|
||||
<link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
|
||||
<link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</link> = true;
|
||||
};
|
||||
<link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl"; # change on first login
|
||||
<link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
|
||||
<link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
|
||||
<link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
|
||||
};
|
||||
</programlisting>
|
||||
</para>
|
||||
|
||||
</section>
|
||||
</chapter>
|
||||
34
nixos/modules/services/web-apps/lemmy.md
Normal file
34
nixos/modules/services/web-apps/lemmy.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Lemmy {#module-services-lemmy}
|
||||
|
||||
Lemmy is a federated alternative to reddit in rust.
|
||||
|
||||
## Quickstart {#module-services-lemmy-quickstart}
|
||||
|
||||
the minimum to start lemmy is
|
||||
|
||||
```nix
|
||||
services.lemmy = {
|
||||
enable = true;
|
||||
settings = {
|
||||
hostname = "lemmy.union.rocks";
|
||||
database.createLocally = true;
|
||||
};
|
||||
jwtSecretPath = "/run/secrets/lemmyJwt";
|
||||
caddy.enable = true;
|
||||
}
|
||||
```
|
||||
|
||||
(note that you can use something like agenix to get your secret jwt to the specified path)
|
||||
|
||||
this will start the backend on port 8536 and the frontend on port 1234.
|
||||
It will expose your instance with a caddy reverse proxy to the hostname you've provided.
|
||||
Postgres will be initialized on that same instance automatically.
|
||||
|
||||
## Usage {#module-services-lemmy-usage}
|
||||
|
||||
On first connection you will be asked to define an admin user.
|
||||
|
||||
## Missing {#module-services-lemmy-missing}
|
||||
|
||||
- Exposing with nginx is not implemented yet.
|
||||
- This has been tested using a local database with a unix socket connection. Using different database settings will likely require modifications
|
||||
236
nixos/modules/services/web-apps/lemmy.nix
Normal file
236
nixos/modules/services/web-apps/lemmy.nix
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.lemmy;
|
||||
settingsFormat = pkgs.formats.json { };
|
||||
in
|
||||
{
|
||||
meta.maintainers = with maintainers; [ happysalada ];
|
||||
# Don't edit the docbook xml directly, edit the md and generate it:
|
||||
# `pandoc lemmy.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > lemmy.xml`
|
||||
meta.doc = ./lemmy.xml;
|
||||
|
||||
options.services.lemmy = {
|
||||
|
||||
enable = mkEnableOption "lemmy a federated alternative to reddit in rust";
|
||||
|
||||
jwtSecretPath = mkOption {
|
||||
type = types.path;
|
||||
description = "Path to read the jwt secret from.";
|
||||
};
|
||||
|
||||
ui = {
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 1234;
|
||||
description = "Port where lemmy-ui should listen for incoming requests.";
|
||||
};
|
||||
};
|
||||
|
||||
caddy.enable = mkEnableOption "exposing lemmy with the caddy reverse proxy";
|
||||
|
||||
settings = mkOption {
|
||||
default = { };
|
||||
description = "Lemmy configuration";
|
||||
|
||||
type = types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
|
||||
options.hostname = mkOption {
|
||||
type = types.str;
|
||||
default = null;
|
||||
description = "The domain name of your instance (eg 'lemmy.ml').";
|
||||
};
|
||||
|
||||
options.port = mkOption {
|
||||
type = types.port;
|
||||
default = 8536;
|
||||
description = "Port where lemmy should listen for incoming requests.";
|
||||
};
|
||||
|
||||
options.federation = {
|
||||
enabled = mkEnableOption "activitypub federation";
|
||||
};
|
||||
|
||||
options.captcha = {
|
||||
enabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable Captcha.";
|
||||
};
|
||||
difficulty = mkOption {
|
||||
type = types.enum [ "easy" "medium" "hard" ];
|
||||
default = "medium";
|
||||
description = "The difficultly of the captcha to solve.";
|
||||
};
|
||||
};
|
||||
|
||||
options.database.createLocally = mkEnableOption "creation of database on the instance";
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
localPostgres = (cfg.settings.database.host == "localhost" || cfg.settings.database.host == "/run/postgresql");
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
services.lemmy.settings = (mapAttrs (name: mkDefault)
|
||||
{
|
||||
bind = "127.0.0.1";
|
||||
tls_enabled = true;
|
||||
pictrs_url = with config.services.pict-rs; "http://${address}:${toString port}";
|
||||
actor_name_max_length = 20;
|
||||
|
||||
rate_limit.message = 180;
|
||||
rate_limit.message_per_second = 60;
|
||||
rate_limit.post = 6;
|
||||
rate_limit.post_per_second = 600;
|
||||
rate_limit.register = 3;
|
||||
rate_limit.register_per_second = 3600;
|
||||
rate_limit.image = 6;
|
||||
rate_limit.image_per_second = 3600;
|
||||
} // {
|
||||
database = mapAttrs (name: mkDefault) {
|
||||
user = "lemmy";
|
||||
host = "/run/postgresql";
|
||||
port = 5432;
|
||||
database = "lemmy";
|
||||
pool_size = 5;
|
||||
};
|
||||
});
|
||||
|
||||
services.postgresql = mkIf localPostgres {
|
||||
enable = mkDefault true;
|
||||
};
|
||||
|
||||
services.pict-rs.enable = true;
|
||||
|
||||
services.caddy = mkIf cfg.caddy.enable {
|
||||
enable = mkDefault true;
|
||||
virtualHosts."${cfg.settings.hostname}" = {
|
||||
extraConfig = ''
|
||||
handle_path /static/* {
|
||||
root * ${pkgs.lemmy-ui}/dist
|
||||
file_server
|
||||
}
|
||||
@for_backend {
|
||||
path /api/* /pictrs/* feeds/* nodeinfo/*
|
||||
}
|
||||
handle @for_backend {
|
||||
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
|
||||
}
|
||||
@post {
|
||||
method POST
|
||||
}
|
||||
handle @post {
|
||||
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
|
||||
}
|
||||
@jsonld {
|
||||
header Accept "application/activity+json"
|
||||
header Accept "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
}
|
||||
handle @jsonld {
|
||||
reverse_proxy 127.0.0.1:${toString cfg.settings.port}
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:${toString cfg.ui.port}
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
assertions = [{
|
||||
assertion = cfg.settings.database.createLocally -> localPostgres;
|
||||
message = "if you want to create the database locally, you need to use a local database";
|
||||
}];
|
||||
|
||||
systemd.services.lemmy = {
|
||||
description = "Lemmy server";
|
||||
|
||||
environment = {
|
||||
LEMMY_CONFIG_LOCATION = "/run/lemmy/config.hjson";
|
||||
|
||||
# Verify how this is used, and don't put the password in the nix store
|
||||
LEMMY_DATABASE_URL = with cfg.settings.database;"postgres:///${database}?host=${host}";
|
||||
};
|
||||
|
||||
documentation = [
|
||||
"https://join-lemmy.org/docs/en/administration/from_scratch.html"
|
||||
"https://join-lemmy.org/docs"
|
||||
];
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
after = [ "pict-rs.service " ] ++ lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
|
||||
|
||||
requires = lib.optionals cfg.settings.database.createLocally [ "lemmy-postgresql.service" ];
|
||||
|
||||
# script is needed here since loadcredential is not accessible on ExecPreStart
|
||||
script = ''
|
||||
${pkgs.coreutils}/bin/install -m 600 ${settingsFormat.generate "config.hjson" cfg.settings} /run/lemmy/config.hjson
|
||||
jwtSecret="$(< $CREDENTIALS_DIRECTORY/jwt_secret )"
|
||||
${pkgs.jq}/bin/jq ".jwt_secret = \"$jwtSecret\"" /run/lemmy/config.hjson | ${pkgs.moreutils}/bin/sponge /run/lemmy/config.hjson
|
||||
${pkgs.lemmy-server}/bin/lemmy_server
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
RuntimeDirectory = "lemmy";
|
||||
LoadCredential = "jwt_secret:${cfg.jwtSecretPath}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.lemmy-ui = {
|
||||
description = "Lemmy ui";
|
||||
|
||||
environment = {
|
||||
LEMMY_UI_HOST = "127.0.0.1:${toString cfg.ui.port}";
|
||||
LEMMY_INTERNAL_HOST = "127.0.0.1:${toString cfg.settings.port}";
|
||||
LEMMY_EXTERNAL_HOST = cfg.settings.hostname;
|
||||
LEMMY_HTTPS = "false";
|
||||
};
|
||||
|
||||
documentation = [
|
||||
"https://join-lemmy.org/docs/en/administration/from_scratch.html"
|
||||
"https://join-lemmy.org/docs"
|
||||
];
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
after = [ "lemmy.service" ];
|
||||
|
||||
requires = [ "lemmy.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
WorkingDirectory = "${pkgs.lemmy-ui}";
|
||||
ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.lemmy-ui}/dist/js/server.js";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.lemmy-postgresql = mkIf cfg.settings.database.createLocally {
|
||||
description = "Lemmy postgresql db";
|
||||
after = [ "postgresql.service" ];
|
||||
partOf = [ "lemmy.service" ];
|
||||
script = with cfg.settings.database; ''
|
||||
PSQL() {
|
||||
${config.services.postgresql.package}/bin/psql --port=${toString cfg.settings.database.port} "$@"
|
||||
}
|
||||
# check if the database already exists
|
||||
if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${database} ; then
|
||||
PSQL -tAc "CREATE ROLE ${user} WITH LOGIN;"
|
||||
PSQL -tAc "CREATE DATABASE ${database} WITH OWNER ${user};"
|
||||
fi
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = config.services.postgresql.superUser;
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
56
nixos/modules/services/web-apps/lemmy.xml
Normal file
56
nixos/modules/services/web-apps/lemmy.xml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-lemmy">
|
||||
<title>Lemmy</title>
|
||||
<para>
|
||||
Lemmy is a federated alternative to reddit in rust.
|
||||
</para>
|
||||
<section xml:id="module-services-lemmy-quickstart">
|
||||
<title>Quickstart</title>
|
||||
<para>
|
||||
the minimum to start lemmy is
|
||||
</para>
|
||||
<programlisting language="bash">
|
||||
services.lemmy = {
|
||||
enable = true;
|
||||
settings = {
|
||||
hostname = "lemmy.union.rocks";
|
||||
database.createLocally = true;
|
||||
};
|
||||
jwtSecretPath = "/run/secrets/lemmyJwt";
|
||||
caddy.enable = true;
|
||||
}
|
||||
</programlisting>
|
||||
<para>
|
||||
(note that you can use something like agenix to get your secret
|
||||
jwt to the specified path)
|
||||
</para>
|
||||
<para>
|
||||
this will start the backend on port 8536 and the frontend on port
|
||||
1234. It will expose your instance with a caddy reverse proxy to
|
||||
the hostname you’ve provided. Postgres will be initialized on that
|
||||
same instance automatically.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-lemmy-usage">
|
||||
<title>Usage</title>
|
||||
<para>
|
||||
On first connection you will be asked to define an admin user.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-lemmy-missing">
|
||||
<title>Missing</title>
|
||||
<itemizedlist spacing="compact">
|
||||
<listitem>
|
||||
<para>
|
||||
Exposing with nginx is not implemented yet.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
This has been tested using a local database with a unix socket
|
||||
connection. Using different database settings will likely
|
||||
require modifications
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
</chapter>
|
||||
280
nixos/modules/services/web-apps/limesurvey.nix
Normal file
280
nixos/modules/services/web-apps/limesurvey.nix
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
|
||||
inherit (lib) literalExpression mapAttrs optional optionalString types;
|
||||
|
||||
cfg = config.services.limesurvey;
|
||||
fpm = config.services.phpfpm.pools.limesurvey;
|
||||
|
||||
user = "limesurvey";
|
||||
group = config.services.httpd.group;
|
||||
stateDir = "/var/lib/limesurvey";
|
||||
|
||||
pkg = pkgs.limesurvey;
|
||||
|
||||
configType = with types; oneOf [ (attrsOf configType) str int bool ] // {
|
||||
description = "limesurvey config type (str, int, bool or attribute set thereof)";
|
||||
};
|
||||
|
||||
limesurveyConfig = pkgs.writeText "config.php" ''
|
||||
<?php
|
||||
return json_decode('${builtins.toJSON cfg.config}', true);
|
||||
?>
|
||||
'';
|
||||
|
||||
mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
|
||||
pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
|
||||
|
||||
in
|
||||
{
|
||||
# interface
|
||||
|
||||
options.services.limesurvey = {
|
||||
enable = mkEnableOption "Limesurvey web application.";
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "mysql" "pgsql" "odbc" "mssql" ];
|
||||
example = "pgsql";
|
||||
default = "mysql";
|
||||
description = "Database engine to use.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = if cfg.database.type == "pgsql" then 5442 else 3306;
|
||||
defaultText = literalExpression "3306";
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "limesurvey";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "limesurvey";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/limesurvey-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default =
|
||||
if mysqlLocal then "/run/mysqld/mysqld.sock"
|
||||
else if pgsqlLocal then "/run/postgresql"
|
||||
else null
|
||||
;
|
||||
defaultText = literalExpression "/run/mysqld/mysqld.sock";
|
||||
description = "Path to the unix socket file to use for authentication.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = cfg.database.type == "mysql";
|
||||
defaultText = literalExpression "true";
|
||||
description = ''
|
||||
Create the database and database user locally.
|
||||
This currently only applies if database type "mysql" is selected.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostName = "survey.example.org";
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the LimeSurvey PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = configType;
|
||||
default = {};
|
||||
description = ''
|
||||
LimeSurvey configuration. Refer to
|
||||
<link xlink:href="https://manual.limesurvey.org/Optional_settings"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
|
||||
message = "services.limesurvey.createLocally is currently only supported for database type 'mysql'";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
||||
message = "services.limesurvey.database.user must be set to ${user} if services.limesurvey.database.createLocally is set true";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.socket != null;
|
||||
message = "services.limesurvey.database.socket must be set if services.limesurvey.database.createLocally is set to true";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = "a password cannot be specified if services.limesurvey.database.createLocally is set to true";
|
||||
}
|
||||
];
|
||||
|
||||
services.limesurvey.config = mapAttrs (name: mkDefault) {
|
||||
runtimePath = "${stateDir}/tmp/runtime";
|
||||
components = {
|
||||
db = {
|
||||
connectionString = "${cfg.database.type}:dbname=${cfg.database.name};host=${if pgsqlLocal then cfg.database.socket else cfg.database.host};port=${toString cfg.database.port}" +
|
||||
optionalString mysqlLocal ";socket=${cfg.database.socket}";
|
||||
username = cfg.database.user;
|
||||
password = mkIf (cfg.database.passwordFile != null) "file_get_contents(\"${toString cfg.database.passwordFile}\");";
|
||||
tablePrefix = "limesurvey_";
|
||||
};
|
||||
assetManager.basePath = "${stateDir}/tmp/assets";
|
||||
urlManager = {
|
||||
urlFormat = "path";
|
||||
showScriptName = false;
|
||||
};
|
||||
};
|
||||
config = {
|
||||
tempdir = "${stateDir}/tmp";
|
||||
uploaddir = "${stateDir}/upload";
|
||||
force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
|
||||
config.defaultlang = "en";
|
||||
};
|
||||
};
|
||||
|
||||
services.mysql = mkIf mysqlLocal {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = {
|
||||
"${cfg.database.name}.*" = "SELECT, CREATE, INSERT, UPDATE, DELETE, ALTER, DROP, INDEX";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.limesurvey = {
|
||||
inherit user group;
|
||||
phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
|
||||
settings = {
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
adminAddr = mkDefault cfg.virtualHost.adminAddr;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
|
||||
documentRoot = mkForce "${pkg}/share/limesurvey";
|
||||
extraConfig = ''
|
||||
Alias "/tmp" "${stateDir}/tmp"
|
||||
<Directory "${stateDir}">
|
||||
AllowOverride all
|
||||
Require all granted
|
||||
Options -Indexes +FollowSymlinks
|
||||
</Directory>
|
||||
|
||||
Alias "/upload" "${stateDir}/upload"
|
||||
<Directory "${stateDir}/upload">
|
||||
AllowOverride all
|
||||
Require all granted
|
||||
Options -Indexes
|
||||
</Directory>
|
||||
|
||||
<Directory "${pkg}/share/limesurvey">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
|
||||
AllowOverride all
|
||||
Options -Indexes
|
||||
DirectoryIndex index.php
|
||||
</Directory>
|
||||
'';
|
||||
} ];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${stateDir} 0750 ${user} ${group} - -"
|
||||
"d ${stateDir}/tmp 0750 ${user} ${group} - -"
|
||||
"d ${stateDir}/tmp/assets 0750 ${user} ${group} - -"
|
||||
"d ${stateDir}/tmp/runtime 0750 ${user} ${group} - -"
|
||||
"d ${stateDir}/tmp/upload 0750 ${user} ${group} - -"
|
||||
"C ${stateDir}/upload 0750 ${user} ${group} - ${pkg}/share/limesurvey/upload"
|
||||
];
|
||||
|
||||
systemd.services.limesurvey-init = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "phpfpm-limesurvey.service" ];
|
||||
after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
environment.LIMESURVEY_CONFIG = limesurveyConfig;
|
||||
script = ''
|
||||
# update or install the database as required
|
||||
${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
|
||||
${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = user;
|
||||
Group = group;
|
||||
Type = "oneshot";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
|
||||
users.users.${user} = {
|
||||
group = group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
640
nixos/modules/services/web-apps/mastodon.nix
Normal file
640
nixos/modules/services/web-apps/mastodon.nix
Normal file
|
|
@ -0,0 +1,640 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.mastodon;
|
||||
# We only want to create a database if we're actually going to connect to it.
|
||||
databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
|
||||
|
||||
env = {
|
||||
RAILS_ENV = "production";
|
||||
NODE_ENV = "production";
|
||||
|
||||
LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
|
||||
|
||||
# mastodon-web concurrency.
|
||||
WEB_CONCURRENCY = toString cfg.webProcesses;
|
||||
MAX_THREADS = toString cfg.webThreads;
|
||||
|
||||
# mastodon-streaming concurrency.
|
||||
STREAMING_CLUSTER_NUM = toString cfg.streamingProcesses;
|
||||
|
||||
DB_USER = cfg.database.user;
|
||||
|
||||
REDIS_HOST = cfg.redis.host;
|
||||
REDIS_PORT = toString(cfg.redis.port);
|
||||
DB_HOST = cfg.database.host;
|
||||
DB_PORT = toString(cfg.database.port);
|
||||
DB_NAME = cfg.database.name;
|
||||
LOCAL_DOMAIN = cfg.localDomain;
|
||||
SMTP_SERVER = cfg.smtp.host;
|
||||
SMTP_PORT = toString(cfg.smtp.port);
|
||||
SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
|
||||
PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
|
||||
PAPERCLIP_ROOT_URL = "/system";
|
||||
ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
|
||||
ES_HOST = cfg.elasticsearch.host;
|
||||
ES_PORT = toString(cfg.elasticsearch.port);
|
||||
|
||||
TRUSTED_PROXY_IP = cfg.trustedProxy;
|
||||
}
|
||||
// (if cfg.smtp.authenticate then { SMTP_LOGIN = cfg.smtp.user; } else {})
|
||||
// cfg.extraConfig;
|
||||
|
||||
systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
|
||||
|
||||
cfgService = {
|
||||
# User and group
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
# State directory and mode
|
||||
StateDirectory = "mastodon";
|
||||
StateDirectoryMode = "0750";
|
||||
# Logs directory and mode
|
||||
LogsDirectory = "mastodon";
|
||||
LogsDirectoryMode = "0750";
|
||||
# Proc filesystem
|
||||
ProcSubset = "pid";
|
||||
ProtectProc = "invisible";
|
||||
# Access write directories
|
||||
UMask = "0027";
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "";
|
||||
# Security
|
||||
NoNewPrivileges = true;
|
||||
# Sandboxing
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = false;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RemoveIPC = true;
|
||||
PrivateMounts = true;
|
||||
# System Call Filtering
|
||||
SystemCallArchitectures = "native";
|
||||
};
|
||||
|
||||
envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
|
||||
(lib.concatLists (lib.mapAttrsToList (name: value:
|
||||
if value != null then [
|
||||
"${name}=\"${toString value}\""
|
||||
] else []
|
||||
) env))));
|
||||
|
||||
mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
|
||||
set -a
|
||||
export RAILS_ROOT="${cfg.package}"
|
||||
source "${envFile}"
|
||||
source /var/lib/mastodon/.secrets_env
|
||||
eval -- "\$@"
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
options = {
|
||||
services.mastodon = {
|
||||
enable = lib.mkEnableOption "Mastodon, a federated social network server";
|
||||
|
||||
configureNginx = lib.mkOption {
|
||||
description = ''
|
||||
Configure nginx as a reverse proxy for mastodon.
|
||||
Note that this makes some assumptions on your setup, and sets settings that will
|
||||
affect other virtualHosts running on your nginx instance, if any.
|
||||
Alternatively you can configure a reverse-proxy of your choice to serve these paths:
|
||||
|
||||
<code>/ -> $(nix-instantiate --eval '<nixpkgs>' -A mastodon.outPath)/public</code>
|
||||
|
||||
<code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.)
|
||||
|
||||
<code>/system/ -> /var/lib/mastodon/public-system/</code>
|
||||
|
||||
<code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code>
|
||||
|
||||
Make sure that websockets are forwarded properly. You might want to set up caching
|
||||
of some requests. Take a look at mastodon's provided nginx configuration at
|
||||
<code>https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf</code>.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
description = ''
|
||||
User under which mastodon runs. If it is set to "mastodon",
|
||||
that user will be created, otherwise it should be set to the
|
||||
name of a user created elsewhere. In both cases,
|
||||
<package>mastodon</package> and a package containing only
|
||||
the shell script <code>mastodon-env</code> will be added to
|
||||
the user's package set. To run a command from
|
||||
<package>mastodon</package> such as <code>tootctl</code>
|
||||
with the environment configured by this module use
|
||||
<code>mastodon-env</code>, as in:
|
||||
|
||||
<code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code>
|
||||
'';
|
||||
type = lib.types.str;
|
||||
default = "mastodon";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
description = ''
|
||||
Group under which mastodon runs.
|
||||
'';
|
||||
type = lib.types.str;
|
||||
default = "mastodon";
|
||||
};
|
||||
|
||||
streamingPort = lib.mkOption {
|
||||
description = "TCP port used by the mastodon-streaming service.";
|
||||
type = lib.types.port;
|
||||
default = 55000;
|
||||
};
|
||||
streamingProcesses = lib.mkOption {
|
||||
description = ''
|
||||
Processes used by the mastodon-streaming service.
|
||||
Defaults to the number of CPU cores minus one.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
};
|
||||
|
||||
webPort = lib.mkOption {
|
||||
description = "TCP port used by the mastodon-web service.";
|
||||
type = lib.types.port;
|
||||
default = 55001;
|
||||
};
|
||||
webProcesses = lib.mkOption {
|
||||
description = "Processes used by the mastodon-web service.";
|
||||
type = lib.types.int;
|
||||
default = 2;
|
||||
};
|
||||
webThreads = lib.mkOption {
|
||||
description = "Threads per process used by the mastodon-web service.";
|
||||
type = lib.types.int;
|
||||
default = 5;
|
||||
};
|
||||
|
||||
sidekiqPort = lib.mkOption {
|
||||
description = "TCP port used by the mastodon-sidekiq service.";
|
||||
type = lib.types.port;
|
||||
default = 55002;
|
||||
};
|
||||
sidekiqThreads = lib.mkOption {
|
||||
description = "Worker threads used by the mastodon-sidekiq service.";
|
||||
type = lib.types.int;
|
||||
default = 25;
|
||||
};
|
||||
|
||||
vapidPublicKeyFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to file containing the public key used for Web Push
|
||||
Voluntary Application Server Identification. A new keypair can
|
||||
be generated by running:
|
||||
|
||||
<code>nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys</code>
|
||||
|
||||
If <option>mastodon.vapidPrivateKeyFile</option>does not
|
||||
exist, it and this file will be created with a new keypair.
|
||||
'';
|
||||
default = "/var/lib/mastodon/secrets/vapid-public-key";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
localDomain = lib.mkOption {
|
||||
description = "The domain serving your Mastodon instance.";
|
||||
example = "social.example.org";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
secretKeyBaseFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to file containing the secret key base.
|
||||
A new secret key base can be generated by running:
|
||||
|
||||
<code>nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret</code>
|
||||
|
||||
If this file does not exist, it will be created with a new secret key base.
|
||||
'';
|
||||
default = "/var/lib/mastodon/secrets/secret-key-base";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
otpSecretFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to file containing the OTP secret.
|
||||
A new OTP secret can be generated by running:
|
||||
|
||||
<code>nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret</code>
|
||||
|
||||
If this file does not exist, it will be created with a new OTP secret.
|
||||
'';
|
||||
default = "/var/lib/mastodon/secrets/otp-secret";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
vapidPrivateKeyFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to file containing the private key used for Web Push
|
||||
Voluntary Application Server Identification. A new keypair can
|
||||
be generated by running:
|
||||
|
||||
<code>nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys</code>
|
||||
|
||||
If this file does not exist, it will be created with a new
|
||||
private key.
|
||||
'';
|
||||
default = "/var/lib/mastodon/secrets/vapid-private-key";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
trustedProxy = lib.mkOption {
|
||||
description = ''
|
||||
You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
|
||||
otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
|
||||
bad because IP addresses are used for important rate limits and security functions.
|
||||
'';
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1";
|
||||
};
|
||||
|
||||
enableUnixSocket = lib.mkOption {
|
||||
description = ''
|
||||
Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
|
||||
is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
|
||||
processes and streaming API (Node.js) processes.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
redis = {
|
||||
createLocally = lib.mkOption {
|
||||
description = "Configure local Redis server for Mastodon.";
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
description = "Redis host.";
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
description = "Redis port.";
|
||||
type = lib.types.port;
|
||||
default = 31637;
|
||||
};
|
||||
};
|
||||
|
||||
database = {
|
||||
createLocally = lib.mkOption {
|
||||
description = "Configure local PostgreSQL database server for Mastodon.";
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/run/postgresql";
|
||||
example = "192.168.23.42";
|
||||
description = "Database host address or unix socket.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 5432;
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "mastodon";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "mastodon";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = "/var/lib/mastodon/secrets/db-password";
|
||||
example = "/run/keys/mastodon-db-password";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
smtp = {
|
||||
createLocally = lib.mkOption {
|
||||
description = "Configure local Postfix SMTP server for Mastodon.";
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
authenticate = lib.mkOption {
|
||||
description = "Authenticate with the SMTP server using username and password.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
description = "SMTP host used when sending emails to users.";
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
description = "SMTP port used when sending emails to users.";
|
||||
type = lib.types.port;
|
||||
default = 25;
|
||||
};
|
||||
|
||||
fromAddress = lib.mkOption {
|
||||
description = ''"From" address used when sending Emails to users.'';
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
description = "SMTP login name.";
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
description = ''
|
||||
Path to file containing the SMTP password.
|
||||
'';
|
||||
default = "/var/lib/mastodon/secrets/smtp-password";
|
||||
example = "/run/keys/mastodon-smtp-password";
|
||||
type = lib.types.str;
|
||||
};
|
||||
};
|
||||
|
||||
elasticsearch = {
|
||||
host = lib.mkOption {
|
||||
description = ''
|
||||
Elasticsearch host.
|
||||
If it is not null, Elasticsearch full text search will be enabled.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
description = "Elasticsearch port.";
|
||||
type = lib.types.port;
|
||||
default = 9200;
|
||||
};
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.mastodon;
|
||||
defaultText = lib.literalExpression "pkgs.mastodon";
|
||||
description = "Mastodon package to use.";
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = {};
|
||||
description = ''
|
||||
Extra environment variables to pass to all mastodon services.
|
||||
'';
|
||||
};
|
||||
|
||||
automaticMigrations = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Do automatic database migrations.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
|
||||
message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.'';
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.mastodon-init-dirs = {
|
||||
script = ''
|
||||
umask 077
|
||||
|
||||
if ! test -f ${cfg.secretKeyBaseFile}; then
|
||||
mkdir -p $(dirname ${cfg.secretKeyBaseFile})
|
||||
bin/rake secret > ${cfg.secretKeyBaseFile}
|
||||
fi
|
||||
if ! test -f ${cfg.otpSecretFile}; then
|
||||
mkdir -p $(dirname ${cfg.otpSecretFile})
|
||||
bin/rake secret > ${cfg.otpSecretFile}
|
||||
fi
|
||||
if ! test -f ${cfg.vapidPrivateKeyFile}; then
|
||||
mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
|
||||
keypair=$(bin/rake webpush:generate_keys)
|
||||
echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
|
||||
echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
|
||||
fi
|
||||
|
||||
cat > /var/lib/mastodon/.secrets_env <<EOF
|
||||
SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
|
||||
OTP_SECRET="$(cat ${cfg.otpSecretFile})"
|
||||
VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
|
||||
VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
|
||||
DB_PASS="$(cat ${cfg.database.passwordFile})"
|
||||
'' + (if cfg.smtp.authenticate then ''
|
||||
SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
|
||||
'' else "") + ''
|
||||
EOF
|
||||
'';
|
||||
environment = env;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
WorkingDirectory = cfg.package;
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
|
||||
script = ''
|
||||
if [ `psql ${cfg.database.name} -c \
|
||||
"select count(*) from pg_class c \
|
||||
join pg_namespace s on s.oid = c.relnamespace \
|
||||
where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
|
||||
and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
|
||||
SAFETY_ASSURED=1 rails db:schema:load
|
||||
rails db:seed
|
||||
else
|
||||
rails db:migrate
|
||||
fi
|
||||
'';
|
||||
path = [ cfg.package pkgs.postgresql ];
|
||||
environment = env;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
EnvironmentFile = "/var/lib/mastodon/.secrets_env";
|
||||
WorkingDirectory = cfg.package;
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []);
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
systemd.services.mastodon-streaming = {
|
||||
after = [ "network.target" ]
|
||||
++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
|
||||
++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
|
||||
description = "Mastodon streaming";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = env // (if cfg.enableUnixSocket
|
||||
then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
|
||||
else { PORT = toString(cfg.streamingPort); }
|
||||
);
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/run-streaming.sh";
|
||||
Restart = "always";
|
||||
RestartSec = 20;
|
||||
EnvironmentFile = "/var/lib/mastodon/.secrets_env";
|
||||
WorkingDirectory = cfg.package;
|
||||
# Runtime directory and mode
|
||||
RuntimeDirectory = "mastodon-streaming";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@memlock" "@resources" ])) "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
};
|
||||
|
||||
systemd.services.mastodon-web = {
|
||||
after = [ "network.target" ]
|
||||
++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
|
||||
++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
|
||||
description = "Mastodon web";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = env // (if cfg.enableUnixSocket
|
||||
then { SOCKET = "/run/mastodon-web/web.socket"; }
|
||||
else { PORT = toString(cfg.webPort); }
|
||||
);
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
|
||||
Restart = "always";
|
||||
RestartSec = 20;
|
||||
EnvironmentFile = "/var/lib/mastodon/.secrets_env";
|
||||
WorkingDirectory = cfg.package;
|
||||
# Runtime directory and mode
|
||||
RuntimeDirectory = "mastodon-web";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
path = with pkgs; [ file imagemagick ffmpeg ];
|
||||
};
|
||||
|
||||
systemd.services.mastodon-sidekiq = {
|
||||
after = [ "network.target" ]
|
||||
++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else [])
|
||||
++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]);
|
||||
description = "Mastodon sidekiq";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = env // {
|
||||
PORT = toString(cfg.sidekiqPort);
|
||||
DB_POOL = toString cfg.sidekiqThreads;
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/sidekiq -c ${toString cfg.sidekiqThreads} -r ${cfg.package}";
|
||||
Restart = "always";
|
||||
RestartSec = 20;
|
||||
EnvironmentFile = "/var/lib/mastodon/.secrets_env";
|
||||
WorkingDirectory = cfg.package;
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
path = with pkgs; [ file imagemagick ffmpeg ];
|
||||
};
|
||||
|
||||
services.nginx = lib.mkIf cfg.configureNginx {
|
||||
enable = true;
|
||||
recommendedProxySettings = true; # required for redirections to work
|
||||
virtualHosts."${cfg.localDomain}" = {
|
||||
root = "${cfg.package}/public/";
|
||||
forceSSL = true; # mastodon only supports https
|
||||
enableACME = true;
|
||||
|
||||
locations."/system/".alias = "/var/lib/mastodon/public-system/";
|
||||
|
||||
locations."/" = {
|
||||
tryFiles = "$uri @proxy";
|
||||
};
|
||||
|
||||
locations."@proxy" = {
|
||||
proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}");
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
|
||||
locations."/api/v1/streaming/" = {
|
||||
proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/");
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
|
||||
enable = true;
|
||||
hostname = lib.mkDefault "${cfg.localDomain}";
|
||||
};
|
||||
services.redis.servers.mastodon = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
|
||||
enable = true;
|
||||
port = cfg.redis.port;
|
||||
bind = "127.0.0.1";
|
||||
};
|
||||
services.postgresql = lib.mkIf databaseActuallyCreateLocally {
|
||||
enable = true;
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.user;
|
||||
ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
|
||||
}
|
||||
];
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
};
|
||||
|
||||
users.users = lib.mkMerge [
|
||||
(lib.mkIf (cfg.user == "mastodon") {
|
||||
mastodon = {
|
||||
isSystemUser = true;
|
||||
home = cfg.package;
|
||||
inherit (cfg) group;
|
||||
};
|
||||
})
|
||||
(lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv ])
|
||||
];
|
||||
|
||||
users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
|
||||
|
||||
}
|
||||
107
nixos/modules/services/web-apps/matomo-doc.xml
Normal file
107
nixos/modules/services/web-apps/matomo-doc.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-matomo">
|
||||
<title>Matomo</title>
|
||||
<para>
|
||||
Matomo is a real-time web analytics application. This module configures
|
||||
php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
|
||||
</para>
|
||||
<para>
|
||||
An automatic setup is not suported by Matomo, so you need to configure Matomo
|
||||
itself in the browser-based Matomo setup.
|
||||
</para>
|
||||
<section xml:id="module-services-matomo-database-setup">
|
||||
<title>Database Setup</title>
|
||||
|
||||
<para>
|
||||
You also need to configure a MariaDB or MySQL database and -user for Matomo
|
||||
yourself, and enter those credentials in your browser. You can use
|
||||
passwordless database authentication via the UNIX_SOCKET authentication
|
||||
plugin with the following SQL commands:
|
||||
<programlisting>
|
||||
# For MariaDB
|
||||
INSTALL PLUGIN unix_socket SONAME 'auth_socket';
|
||||
CREATE DATABASE matomo;
|
||||
CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
|
||||
GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
|
||||
|
||||
# For MySQL
|
||||
INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
|
||||
CREATE DATABASE matomo;
|
||||
CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
|
||||
GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
|
||||
</programlisting>
|
||||
Then fill in <literal>matomo</literal> as database user and database name,
|
||||
and leave the password field blank. This authentication works by allowing
|
||||
only the <literal>matomo</literal> unix user to authenticate as the
|
||||
<literal>matomo</literal> database user (without needing a password), but no
|
||||
other users. For more information on passwordless login, see
|
||||
<link xlink:href="https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/" />.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Of course, you can use password based authentication as well, e.g. when the
|
||||
database is not on the same host.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-matomo-archive-processing">
|
||||
<title>Archive Processing</title>
|
||||
|
||||
<para>
|
||||
This module comes with the systemd service
|
||||
<literal>matomo-archive-processing.service</literal> and a timer that
|
||||
automatically triggers archive processing every hour. This means that you
|
||||
can safely
|
||||
<link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
|
||||
disable browser triggers for Matomo archiving </link> at
|
||||
<literal>Administration > System > General Settings</literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
With automatic archive processing, you can now also enable to
|
||||
<link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
|
||||
delete old visitor logs </link> at <literal>Administration > System >
|
||||
Privacy</literal>, but make sure that you run <literal>systemctl start
|
||||
matomo-archive-processing.service</literal> at least once without errors if
|
||||
you have already collected data before, so that the reports get archived
|
||||
before the source data gets deleted.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-matomo-backups">
|
||||
<title>Backup</title>
|
||||
|
||||
<para>
|
||||
You only need to take backups of your MySQL database and the
|
||||
<filename>/var/lib/matomo/config/config.ini.php</filename> file. Use a user
|
||||
in the <literal>matomo</literal> group or root to access the file. For more
|
||||
information, see
|
||||
<link xlink:href="https://matomo.org/faq/how-to-install/faq_138/" />.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-matomo-issues">
|
||||
<title>Issues</title>
|
||||
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
Matomo will warn you that the JavaScript tracker is not writable. This is
|
||||
because it's located in the read-only nix store. You can safely ignore
|
||||
this, unless you need a plugin that needs JavaScript tracker access.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
<section xml:id="module-services-matomo-other-web-servers">
|
||||
<title>Using other Web Servers than nginx</title>
|
||||
|
||||
<para>
|
||||
You can use other web servers by forwarding calls for
|
||||
<filename>index.php</filename> and <filename>piwik.php</filename> to the
|
||||
<literal><link linkend="opt-services.phpfpm.pools._name_.socket">services.phpfpm.pools.<name>.socket</link></literal> fastcgi unix socket. You can use
|
||||
the nginx configuration in the module code as a reference to what else
|
||||
should be configured.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
335
nixos/modules/services/web-apps/matomo.nix
Normal file
335
nixos/modules/services/web-apps/matomo.nix
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
{ config, lib, options, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.matomo;
|
||||
fpm = config.services.phpfpm.pools.${pool};
|
||||
|
||||
user = "matomo";
|
||||
dataDir = "/var/lib/${user}";
|
||||
deprecatedDataDir = "/var/lib/piwik";
|
||||
|
||||
pool = user;
|
||||
phpExecutionUnit = "phpfpm-${pool}";
|
||||
databaseService = "mysql.service";
|
||||
|
||||
fqdn = if config.networking.domain != null then config.networking.fqdn else config.networking.hostName;
|
||||
|
||||
in {
|
||||
imports = [
|
||||
(mkRenamedOptionModule [ "services" "piwik" "enable" ] [ "services" "matomo" "enable" ])
|
||||
(mkRenamedOptionModule [ "services" "piwik" "webServerUser" ] [ "services" "matomo" "webServerUser" ])
|
||||
(mkRemovedOptionModule [ "services" "piwik" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
|
||||
(mkRemovedOptionModule [ "services" "matomo" "phpfpmProcessManagerConfig" ] "Use services.phpfpm.pools.<name>.settings")
|
||||
(mkRenamedOptionModule [ "services" "piwik" "nginx" ] [ "services" "matomo" "nginx" ])
|
||||
(mkRenamedOptionModule [ "services" "matomo" "periodicArchiveProcessingUrl" ] [ "services" "matomo" "hostname" ])
|
||||
];
|
||||
|
||||
options = {
|
||||
services.matomo = {
|
||||
# NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963
|
||||
# Matomo issue for automatic Matomo setup: https://github.com/matomo-org/matomo/issues/10257
|
||||
# TODO: find a nice way to do this when more NixOS MySQL and / or Matomo automatic setup stuff is implemented.
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable Matomo web analytics with php-fpm backend.
|
||||
Either the nginx option or the webServerUser option is mandatory.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
description = ''
|
||||
Matomo package for the service to use.
|
||||
This can be used to point to newer releases from nixos-unstable,
|
||||
as they don't get backported if they are not security-relevant.
|
||||
'';
|
||||
default = pkgs.matomo;
|
||||
defaultText = literalExpression "pkgs.matomo";
|
||||
};
|
||||
|
||||
webServerUser = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "lighttpd";
|
||||
description = ''
|
||||
Name of the web server user that forwards requests to <option>services.phpfpm.pools.<name>.socket</option> the fastcgi socket for Matomo if the nginx
|
||||
option is not used. Either this option or the nginx option is mandatory.
|
||||
If you want to use another webserver than nginx, you need to set this to that server's user
|
||||
and pass fastcgi requests to `index.php`, `matomo.php` and `piwik.php` (legacy name) to this socket.
|
||||
'';
|
||||
};
|
||||
|
||||
periodicArchiveProcessing = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Enable periodic archive processing, which generates aggregated reports from the visits.
|
||||
|
||||
This means that you can safely disable browser triggers for Matomo archiving,
|
||||
and safely enable to delete old visitor logs.
|
||||
Before deleting visitor logs,
|
||||
make sure though that you run <literal>systemctl start matomo-archive-processing.service</literal>
|
||||
at least once without errors if you have already collected data before.
|
||||
'';
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
default = "${user}.${fqdn}";
|
||||
defaultText = literalExpression ''
|
||||
if config.${options.networking.domain} != null
|
||||
then "${user}.''${config.${options.networking.fqdn}}"
|
||||
else "${user}.''${config.${options.networking.hostName}}"
|
||||
'';
|
||||
example = "matomo.yourdomain.org";
|
||||
description = ''
|
||||
URL of the host, without https prefix. You may want to change it if you
|
||||
run Matomo on a different URL than matomo.yourdomain.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
type = types.nullOr (types.submodule (
|
||||
recursiveUpdate
|
||||
(import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
|
||||
{
|
||||
# enable encryption by default,
|
||||
# as sensitive login and Matomo data should not be transmitted in clear text.
|
||||
options.forceSSL.default = true;
|
||||
options.enableACME.default = true;
|
||||
}
|
||||
)
|
||||
);
|
||||
default = null;
|
||||
example = literalExpression ''
|
||||
{
|
||||
serverAliases = [
|
||||
"matomo.''${config.networking.domain}"
|
||||
"stats.''${config.networking.domain}"
|
||||
];
|
||||
enableACME = false;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
With this option, you can customize an nginx virtualHost which already has sensible defaults for Matomo.
|
||||
Either this option or the webServerUser option is mandatory.
|
||||
Set this to {} to just enable the virtualHost if you don't need any customization.
|
||||
If enabled, then by default, the <option>serverName</option> is
|
||||
<literal>''${user}.''${config.networking.hostName}.''${config.networking.domain}</literal>,
|
||||
SSL is active, and certificates are acquired via ACME.
|
||||
If this is set to null (the default), no nginx virtualHost will be configured.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
warnings = mkIf (cfg.nginx != null && cfg.webServerUser != null) [
|
||||
"If services.matomo.nginx is set, services.matomo.nginx.webServerUser is ignored and should be removed."
|
||||
];
|
||||
|
||||
assertions = [ {
|
||||
assertion = cfg.nginx != null || cfg.webServerUser != null;
|
||||
message = "Either services.matomo.nginx or services.matomo.nginx.webServerUser is mandatory";
|
||||
}];
|
||||
|
||||
users.users.${user} = {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = dataDir;
|
||||
group = user;
|
||||
};
|
||||
users.groups.${user} = {};
|
||||
|
||||
systemd.services.matomo-setup-update = {
|
||||
# everything needs to set up and up to date before Matomo php files are executed
|
||||
requiredBy = [ "${phpExecutionUnit}.service" ];
|
||||
before = [ "${phpExecutionUnit}.service" ];
|
||||
# the update part of the script can only work if the database is already up and running
|
||||
requires = [ databaseService ];
|
||||
after = [ databaseService ];
|
||||
path = [ cfg.package ];
|
||||
environment.PIWIK_USER_PATH = dataDir;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
# hide especially config.ini.php from other
|
||||
UMask = "0007";
|
||||
# TODO: might get renamed to MATOMO_USER_PATH in future versions
|
||||
# chown + chmod in preStart needs root
|
||||
PermissionsStartOnly = true;
|
||||
};
|
||||
|
||||
# correct ownership and permissions in case they're not correct anymore,
|
||||
# e.g. after restoring from backup or moving from another system.
|
||||
# Note that ${dataDir}/config/config.ini.php might contain the MySQL password.
|
||||
preStart = ''
|
||||
# migrate data from piwik to Matomo folder
|
||||
if [ -d ${deprecatedDataDir} ]; then
|
||||
echo "Migrating from ${deprecatedDataDir} to ${dataDir}"
|
||||
mv -T ${deprecatedDataDir} ${dataDir}
|
||||
fi
|
||||
chown -R ${user}:${user} ${dataDir}
|
||||
chmod -R ug+rwX,o-rwx ${dataDir}
|
||||
|
||||
if [ -e ${dataDir}/current-package ]; then
|
||||
CURRENT_PACKAGE=$(readlink ${dataDir}/current-package)
|
||||
NEW_PACKAGE=${cfg.package}
|
||||
if [ "$CURRENT_PACKAGE" != "$NEW_PACKAGE" ]; then
|
||||
# keeping tmp arround between upgrades seems to bork stuff, so delete it
|
||||
rm -rf ${dataDir}/tmp
|
||||
fi
|
||||
elif [ -e ${dataDir}/tmp ]; then
|
||||
# upgrade from 4.4.1
|
||||
rm -rf ${dataDir}/tmp
|
||||
fi
|
||||
ln -sfT ${cfg.package} ${dataDir}/current-package
|
||||
'';
|
||||
script = ''
|
||||
# Use User-Private Group scheme to protect Matomo data, but allow administration / backup via 'matomo' group
|
||||
# Copy config folder
|
||||
chmod g+s "${dataDir}"
|
||||
cp -r "${cfg.package}/share/config" "${dataDir}/"
|
||||
mkdir -p "${dataDir}/misc"
|
||||
chmod -R u+rwX,g+rwX,o-rwx "${dataDir}"
|
||||
|
||||
# check whether user setup has already been done
|
||||
if test -f "${dataDir}/config/config.ini.php"; then
|
||||
# then execute possibly pending database upgrade
|
||||
matomo-console core:update --yes
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# If this is run regularly via the timer,
|
||||
# 'Browser trigger archiving' can be disabled in Matomo UI > Settings > General Settings.
|
||||
systemd.services.matomo-archive-processing = {
|
||||
description = "Archive Matomo reports";
|
||||
# the archiving can only work if the database is already up and running
|
||||
requires = [ databaseService ];
|
||||
after = [ databaseService ];
|
||||
|
||||
# TODO: might get renamed to MATOMO_USER_PATH in future versions
|
||||
environment.PIWIK_USER_PATH = dataDir;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
UMask = "0007";
|
||||
CPUSchedulingPolicy = "idle";
|
||||
IOSchedulingClass = "idle";
|
||||
ExecStart = "${cfg.package}/bin/matomo-console core:archive --url=https://${cfg.hostname}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.matomo-archive-processing = mkIf cfg.periodicArchiveProcessing {
|
||||
description = "Automatically archive Matomo reports every hour";
|
||||
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "hourly";
|
||||
Persistent = "yes";
|
||||
AccuracySec = "10m";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.${phpExecutionUnit} = {
|
||||
# stop phpfpm on package upgrade, do database upgrade via matomo-setup-update, and then restart
|
||||
restartTriggers = [ cfg.package ];
|
||||
# stop config.ini.php from getting written with read permission for others
|
||||
serviceConfig.UMask = "0007";
|
||||
};
|
||||
|
||||
services.phpfpm.pools = let
|
||||
# workaround for when both are null and need to generate a string,
|
||||
# which is illegal, but as assertions apparently are being triggered *after* config generation,
|
||||
# we have to avoid already throwing errors at this previous stage.
|
||||
socketOwner = if (cfg.nginx != null) then config.services.nginx.user
|
||||
else if (cfg.webServerUser != null) then cfg.webServerUser else "";
|
||||
in {
|
||||
${pool} = {
|
||||
inherit user;
|
||||
phpOptions = ''
|
||||
error_log = 'stderr'
|
||||
log_errors = on
|
||||
'';
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = socketOwner;
|
||||
"listen.group" = "root";
|
||||
"listen.mode" = "0660";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 10;
|
||||
"pm.min_spare_servers" = 5;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = true;
|
||||
};
|
||||
phpEnv.PIWIK_USER_PATH = dataDir;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
|
||||
# References:
|
||||
# https://fralef.me/piwik-hardening-with-nginx-and-php-fpm.html
|
||||
# https://github.com/perusio/piwik-nginx
|
||||
"${cfg.hostname}" = mkMerge [ cfg.nginx {
|
||||
# don't allow to override the root easily, as it will almost certainly break Matomo.
|
||||
# disadvantage: not shown as default in docs.
|
||||
root = mkForce "${cfg.package}/share";
|
||||
|
||||
# define locations here instead of as the submodule option's default
|
||||
# so that they can easily be extended with additional locations if required
|
||||
# without needing to redefine the Matomo ones.
|
||||
# disadvantage: not shown as default in docs.
|
||||
locations."/" = {
|
||||
index = "index.php";
|
||||
};
|
||||
# allow index.php for webinterface
|
||||
locations."= /index.php".extraConfig = ''
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
'';
|
||||
# allow matomo.php for tracking
|
||||
locations."= /matomo.php".extraConfig = ''
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
'';
|
||||
# allow piwik.php for tracking (deprecated name)
|
||||
locations."= /piwik.php".extraConfig = ''
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
'';
|
||||
# Any other attempt to access any php files is forbidden
|
||||
locations."~* ^.+\\.php$".extraConfig = ''
|
||||
return 403;
|
||||
'';
|
||||
# Disallow access to unneeded directories
|
||||
# config and tmp are already removed
|
||||
locations."~ ^/(?:core|lang|misc)/".extraConfig = ''
|
||||
return 403;
|
||||
'';
|
||||
# Disallow access to several helper files
|
||||
locations."~* \\.(?:bat|git|ini|sh|txt|tpl|xml|md)$".extraConfig = ''
|
||||
return 403;
|
||||
'';
|
||||
# No crawling of this site for bots that obey robots.txt - no useful information here.
|
||||
locations."= /robots.txt".extraConfig = ''
|
||||
return 200 "User-agent: *\nDisallow: /\n";
|
||||
'';
|
||||
# let browsers cache matomo.js
|
||||
locations."= /matomo.js".extraConfig = ''
|
||||
expires 1M;
|
||||
'';
|
||||
# let browsers cache piwik.js (deprecated name)
|
||||
locations."= /piwik.js".extraConfig = ''
|
||||
expires 1M;
|
||||
'';
|
||||
}];
|
||||
};
|
||||
};
|
||||
|
||||
meta = {
|
||||
doc = ./matomo-doc.xml;
|
||||
maintainers = with lib.maintainers; [ florianjacob ];
|
||||
};
|
||||
}
|
||||
344
nixos/modules/services/web-apps/mattermost.nix
Normal file
344
nixos/modules/services/web-apps/mattermost.nix
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.mattermost;
|
||||
|
||||
database = "postgres://${cfg.localDatabaseUser}:${cfg.localDatabasePassword}@localhost:5432/${cfg.localDatabaseName}?sslmode=disable&connect_timeout=10";
|
||||
|
||||
postgresPackage = config.services.postgresql.package;
|
||||
|
||||
createDb = {
|
||||
statePath ? cfg.statePath,
|
||||
localDatabaseUser ? cfg.localDatabaseUser,
|
||||
localDatabasePassword ? cfg.localDatabasePassword,
|
||||
localDatabaseName ? cfg.localDatabaseName,
|
||||
useSudo ? true
|
||||
}: ''
|
||||
if ! test -e ${escapeShellArg "${statePath}/.db-created"}; then
|
||||
${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
|
||||
${postgresPackage}/bin/psql postgres -c \
|
||||
"CREATE ROLE ${localDatabaseUser} WITH LOGIN NOCREATEDB NOCREATEROLE ENCRYPTED PASSWORD '${localDatabasePassword}'"
|
||||
${lib.optionalString useSudo "${pkgs.sudo}/bin/sudo -u ${escapeShellArg config.services.postgresql.superUser} \\"}
|
||||
${postgresPackage}/bin/createdb \
|
||||
--owner ${escapeShellArg localDatabaseUser} ${escapeShellArg localDatabaseName}
|
||||
touch ${escapeShellArg "${statePath}/.db-created"}
|
||||
fi
|
||||
'';
|
||||
|
||||
mattermostPluginDerivations = with pkgs;
|
||||
map (plugin: stdenv.mkDerivation {
|
||||
name = "mattermost-plugin";
|
||||
installPhase = ''
|
||||
mkdir -p $out/share
|
||||
cp ${plugin} $out/share/plugin.tar.gz
|
||||
'';
|
||||
dontUnpack = true;
|
||||
dontPatch = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
preferLocalBuild = true;
|
||||
}) cfg.plugins;
|
||||
|
||||
mattermostPlugins = with pkgs;
|
||||
if mattermostPluginDerivations == [] then null
|
||||
else stdenv.mkDerivation {
|
||||
name = "${cfg.package.name}-plugins";
|
||||
nativeBuildInputs = [
|
||||
autoPatchelfHook
|
||||
] ++ mattermostPluginDerivations;
|
||||
buildInputs = [
|
||||
cfg.package
|
||||
];
|
||||
installPhase = ''
|
||||
mkdir -p $out/data/plugins
|
||||
plugins=(${escapeShellArgs (map (plugin: "${plugin}/share/plugin.tar.gz") mattermostPluginDerivations)})
|
||||
for plugin in "''${plugins[@]}"; do
|
||||
hash="$(sha256sum "$plugin" | cut -d' ' -f1)"
|
||||
mkdir -p "$hash"
|
||||
tar -C "$hash" -xzf "$plugin"
|
||||
autoPatchelf "$hash"
|
||||
GZIP_OPT=-9 tar -C "$hash" -cvzf "$out/data/plugins/$hash.tar.gz" .
|
||||
rm -rf "$hash"
|
||||
done
|
||||
'';
|
||||
|
||||
dontUnpack = true;
|
||||
dontPatch = true;
|
||||
dontConfigure = true;
|
||||
dontBuild = true;
|
||||
preferLocalBuild = true;
|
||||
};
|
||||
|
||||
mattermostConfWithoutPlugins = recursiveUpdate
|
||||
{ ServiceSettings.SiteURL = cfg.siteUrl;
|
||||
ServiceSettings.ListenAddress = cfg.listenAddress;
|
||||
TeamSettings.SiteName = cfg.siteName;
|
||||
SqlSettings.DriverName = "postgres";
|
||||
SqlSettings.DataSource = database;
|
||||
PluginSettings.Directory = "${cfg.statePath}/plugins/server";
|
||||
PluginSettings.ClientDirectory = "${cfg.statePath}/plugins/client";
|
||||
}
|
||||
cfg.extraConfig;
|
||||
|
||||
mattermostConf = recursiveUpdate
|
||||
mattermostConfWithoutPlugins
|
||||
(
|
||||
if mattermostPlugins == null then {}
|
||||
else {
|
||||
PluginSettings = {
|
||||
Enable = true;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
mattermostConfJSON = pkgs.writeText "mattermost-config.json" (builtins.toJSON mattermostConf);
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.mattermost = {
|
||||
enable = mkEnableOption "Mattermost chat server";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.mattermost;
|
||||
defaultText = "pkgs.mattermost";
|
||||
description = "Mattermost derivation to use.";
|
||||
};
|
||||
|
||||
statePath = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/mattermost";
|
||||
description = "Mattermost working directory";
|
||||
};
|
||||
|
||||
siteUrl = mkOption {
|
||||
type = types.str;
|
||||
example = "https://chat.example.com";
|
||||
description = ''
|
||||
URL this Mattermost instance is reachable under, without trailing slash.
|
||||
'';
|
||||
};
|
||||
|
||||
siteName = mkOption {
|
||||
type = types.str;
|
||||
default = "Mattermost";
|
||||
description = "Name of this Mattermost site.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = ":8065";
|
||||
example = "[::1]:8065";
|
||||
description = ''
|
||||
Address and port this Mattermost instance listens to.
|
||||
'';
|
||||
};
|
||||
|
||||
mutableConfig = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether the Mattermost config.json is writeable by Mattermost.
|
||||
|
||||
Most of the settings can be edited in the system console of
|
||||
Mattermost if this option is enabled. A template config using
|
||||
the options specified in services.mattermost will be generated
|
||||
but won't be overwritten on changes or rebuilds.
|
||||
|
||||
If this option is disabled, changes in the system console won't
|
||||
be possible (default). If an config.json is present, it will be
|
||||
overwritten!
|
||||
'';
|
||||
};
|
||||
|
||||
preferNixConfig = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
If both mutableConfig and this option are set, the Nix configuration
|
||||
will take precedence over any settings configured in the server
|
||||
console.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
Addtional configuration options as Nix attribute set in config.json schema.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf (types.oneOf [types.path types.package]);
|
||||
default = [];
|
||||
example = "[ ./com.github.moussetc.mattermost.plugin.giphy-2.0.0.tar.gz ]";
|
||||
description = ''
|
||||
Plugins to add to the configuration. Overrides any installed if non-null.
|
||||
This is a list of paths to .tar.gz files or derivations evaluating to
|
||||
.tar.gz files.
|
||||
'';
|
||||
};
|
||||
|
||||
localDatabaseCreate = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Create a local PostgreSQL database for Mattermost automatically.
|
||||
'';
|
||||
};
|
||||
|
||||
localDatabaseName = mkOption {
|
||||
type = types.str;
|
||||
default = "mattermost";
|
||||
description = ''
|
||||
Local Mattermost database name.
|
||||
'';
|
||||
};
|
||||
|
||||
localDatabaseUser = mkOption {
|
||||
type = types.str;
|
||||
default = "mattermost";
|
||||
description = ''
|
||||
Local Mattermost database username.
|
||||
'';
|
||||
};
|
||||
|
||||
localDatabasePassword = mkOption {
|
||||
type = types.str;
|
||||
default = "mmpgsecret";
|
||||
description = ''
|
||||
Password for local Mattermost database user.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "mattermost";
|
||||
description = ''
|
||||
User which runs the Mattermost service.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "mattermost";
|
||||
description = ''
|
||||
Group which runs the Mattermost service.
|
||||
'';
|
||||
};
|
||||
|
||||
matterircd = {
|
||||
enable = mkEnableOption "Mattermost IRC bridge";
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.matterircd;
|
||||
defaultText = "pkgs.matterircd";
|
||||
description = "matterircd derivation to use.";
|
||||
};
|
||||
parameters = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "-mmserver chat.example.com" "-bind [::]:6667" ];
|
||||
description = ''
|
||||
Set commandline parameters to pass to matterircd. See
|
||||
https://github.com/42wim/matterircd#usage for more information.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
(mkIf cfg.enable {
|
||||
users.users = optionalAttrs (cfg.user == "mattermost") {
|
||||
mattermost = {
|
||||
group = cfg.group;
|
||||
uid = config.ids.uids.mattermost;
|
||||
home = cfg.statePath;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == "mattermost") {
|
||||
mattermost.gid = config.ids.gids.mattermost;
|
||||
};
|
||||
|
||||
services.postgresql.enable = cfg.localDatabaseCreate;
|
||||
|
||||
# The systemd service will fail to execute the preStart hook
|
||||
# if the WorkingDirectory does not exist
|
||||
system.activationScripts.mattermost = ''
|
||||
mkdir -p "${cfg.statePath}"
|
||||
'';
|
||||
|
||||
systemd.services.mattermost = {
|
||||
description = "Mattermost chat service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" "postgresql.service" ];
|
||||
|
||||
preStart = ''
|
||||
mkdir -p "${cfg.statePath}"/{data,config,logs,plugins}
|
||||
mkdir -p "${cfg.statePath}/plugins"/{client,server}
|
||||
ln -sf ${cfg.package}/{bin,fonts,i18n,templates,client} "${cfg.statePath}"
|
||||
'' + lib.optionalString (mattermostPlugins != null) ''
|
||||
rm -rf "${cfg.statePath}/data/plugins"
|
||||
ln -sf ${mattermostPlugins}/data/plugins "${cfg.statePath}/data"
|
||||
'' + lib.optionalString (!cfg.mutableConfig) ''
|
||||
rm -f "${cfg.statePath}/config/config.json"
|
||||
${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
|
||||
'' + lib.optionalString cfg.mutableConfig ''
|
||||
if ! test -e "${cfg.statePath}/config/.initial-created"; then
|
||||
rm -f ${cfg.statePath}/config/config.json
|
||||
${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${cfg.package}/config/config.json ${mattermostConfJSON} > "${cfg.statePath}/config/config.json"
|
||||
touch "${cfg.statePath}/config/.initial-created"
|
||||
fi
|
||||
'' + lib.optionalString (cfg.mutableConfig && cfg.preferNixConfig) ''
|
||||
new_config="$(${pkgs.jq}/bin/jq -s '.[0] * .[1]' "${cfg.statePath}/config/config.json" ${mattermostConfJSON})"
|
||||
|
||||
rm -f "${cfg.statePath}/config/config.json"
|
||||
echo "$new_config" > "${cfg.statePath}/config/config.json"
|
||||
'' + lib.optionalString cfg.localDatabaseCreate (createDb {}) + ''
|
||||
# Don't change permissions recursively on the data, current, and symlinked directories (see ln -sf command above).
|
||||
# This dramatically decreases startup times for installations with a lot of files.
|
||||
find . -maxdepth 1 -not -name data -not -name client -not -name templates -not -name i18n -not -name fonts -not -name bin -not -name . \
|
||||
-exec chown "${cfg.user}:${cfg.group}" -R {} \; -exec chmod u+rw,g+r,o-rwx -R {} \;
|
||||
|
||||
chown "${cfg.user}:${cfg.group}" "${cfg.statePath}/data" .
|
||||
chmod u+rw,g+r,o-rwx "${cfg.statePath}/data" .
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
PermissionsStartOnly = true;
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${cfg.package}/bin/mattermost";
|
||||
WorkingDirectory = "${cfg.statePath}";
|
||||
Restart = "always";
|
||||
RestartSec = "10";
|
||||
LimitNOFILE = "49152";
|
||||
};
|
||||
unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
|
||||
};
|
||||
})
|
||||
(mkIf cfg.matterircd.enable {
|
||||
systemd.services.matterircd = {
|
||||
description = "Mattermost IRC bridge service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
User = "nobody";
|
||||
Group = "nogroup";
|
||||
ExecStart = "${cfg.matterircd.package}/bin/matterircd ${escapeShellArgs cfg.matterircd.parameters}";
|
||||
WorkingDirectory = "/tmp";
|
||||
PrivateTmp = true;
|
||||
Restart = "always";
|
||||
RestartSec = "5";
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
475
nixos/modules/services/web-apps/mediawiki.nix
Normal file
475
nixos/modules/services/web-apps/mediawiki.nix
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption;
|
||||
inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionals optionalString types;
|
||||
|
||||
cfg = config.services.mediawiki;
|
||||
fpm = config.services.phpfpm.pools.mediawiki;
|
||||
user = "mediawiki";
|
||||
group = config.services.httpd.group;
|
||||
cacheDir = "/var/cache/mediawiki";
|
||||
stateDir = "/var/lib/mediawiki";
|
||||
|
||||
pkg = pkgs.stdenv.mkDerivation rec {
|
||||
pname = "mediawiki-full";
|
||||
version = src.version;
|
||||
src = cfg.package;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r * $out/
|
||||
|
||||
rm -rf $out/share/mediawiki/skins/*
|
||||
rm -rf $out/share/mediawiki/extensions/*
|
||||
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: ''
|
||||
ln -s ${v} $out/share/mediawiki/skins/${k}
|
||||
'') cfg.skins)}
|
||||
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: ''
|
||||
ln -s ${if v != null then v else "$src/share/mediawiki/extensions/${k}"} $out/share/mediawiki/extensions/${k}
|
||||
'') cfg.extensions)}
|
||||
'';
|
||||
};
|
||||
|
||||
mediawikiScripts = pkgs.runCommand "mediawiki-scripts" {
|
||||
buildInputs = [ pkgs.makeWrapper ];
|
||||
preferLocalBuild = true;
|
||||
} ''
|
||||
mkdir -p $out/bin
|
||||
for i in changePassword.php createAndPromote.php userOptions.php edit.php nukePage.php update.php; do
|
||||
makeWrapper ${pkgs.php}/bin/php $out/bin/mediawiki-$(basename $i .php) \
|
||||
--set MEDIAWIKI_CONFIG ${mediawikiConfig} \
|
||||
--add-flags ${pkg}/share/mediawiki/maintenance/$i
|
||||
done
|
||||
'';
|
||||
|
||||
mediawikiConfig = pkgs.writeText "LocalSettings.php" ''
|
||||
<?php
|
||||
# Protect against web entry
|
||||
if ( !defined( 'MEDIAWIKI' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$wgSitename = "${cfg.name}";
|
||||
$wgMetaNamespace = false;
|
||||
|
||||
## The URL base path to the directory containing the wiki;
|
||||
## defaults for all runtime URL paths are based off of this.
|
||||
## For more information on customizing the URLs
|
||||
## (like /w/index.php/Page_title to /wiki/Page_title) please see:
|
||||
## https://www.mediawiki.org/wiki/Manual:Short_URL
|
||||
$wgScriptPath = "";
|
||||
|
||||
## The protocol and server name to use in fully-qualified URLs
|
||||
$wgServer = "${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}";
|
||||
|
||||
## The URL path to static resources (images, scripts, etc.)
|
||||
$wgResourceBasePath = $wgScriptPath;
|
||||
|
||||
## The URL path to the logo. Make sure you change this from the default,
|
||||
## or else you'll overwrite your logo when you upgrade!
|
||||
$wgLogo = "$wgResourceBasePath/resources/assets/wiki.png";
|
||||
|
||||
## UPO means: this is also a user preference option
|
||||
|
||||
$wgEnableEmail = true;
|
||||
$wgEnableUserEmail = true; # UPO
|
||||
|
||||
$wgEmergencyContact = "${if cfg.virtualHost.adminAddr != null then cfg.virtualHost.adminAddr else config.services.httpd.adminAddr}";
|
||||
$wgPasswordSender = $wgEmergencyContact;
|
||||
|
||||
$wgEnotifUserTalk = false; # UPO
|
||||
$wgEnotifWatchlist = false; # UPO
|
||||
$wgEmailAuthentication = true;
|
||||
|
||||
## Database settings
|
||||
$wgDBtype = "${cfg.database.type}";
|
||||
$wgDBserver = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
|
||||
$wgDBname = "${cfg.database.name}";
|
||||
$wgDBuser = "${cfg.database.user}";
|
||||
${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
|
||||
|
||||
${optionalString (cfg.database.type == "mysql" && cfg.database.tablePrefix != null) ''
|
||||
# MySQL specific settings
|
||||
$wgDBprefix = "${cfg.database.tablePrefix}";
|
||||
''}
|
||||
|
||||
${optionalString (cfg.database.type == "mysql") ''
|
||||
# MySQL table options to use during installation or update
|
||||
$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
|
||||
''}
|
||||
|
||||
## Shared memory settings
|
||||
$wgMainCacheType = CACHE_NONE;
|
||||
$wgMemCachedServers = [];
|
||||
|
||||
${optionalString (cfg.uploadsDir != null) ''
|
||||
$wgEnableUploads = true;
|
||||
$wgUploadDirectory = "${cfg.uploadsDir}";
|
||||
''}
|
||||
|
||||
$wgUseImageMagick = true;
|
||||
$wgImageMagickConvertCommand = "${pkgs.imagemagick}/bin/convert";
|
||||
|
||||
# InstantCommons allows wiki to use images from https://commons.wikimedia.org
|
||||
$wgUseInstantCommons = false;
|
||||
|
||||
# Periodically send a pingback to https://www.mediawiki.org/ with basic data
|
||||
# about this MediaWiki instance. The Wikimedia Foundation shares this data
|
||||
# with MediaWiki developers to help guide future development efforts.
|
||||
$wgPingback = true;
|
||||
|
||||
## If you use ImageMagick (or any other shell command) on a
|
||||
## Linux server, this will need to be set to the name of an
|
||||
## available UTF-8 locale
|
||||
$wgShellLocale = "C.UTF-8";
|
||||
|
||||
## Set $wgCacheDirectory to a writable directory on the web server
|
||||
## to make your wiki go slightly faster. The directory should not
|
||||
## be publically accessible from the web.
|
||||
$wgCacheDirectory = "${cacheDir}";
|
||||
|
||||
# Site language code, should be one of the list in ./languages/data/Names.php
|
||||
$wgLanguageCode = "en";
|
||||
|
||||
$wgSecretKey = file_get_contents("${stateDir}/secret.key");
|
||||
|
||||
# Changing this will log out all existing sessions.
|
||||
$wgAuthenticationTokenVersion = "";
|
||||
|
||||
## For attaching licensing metadata to pages, and displaying an
|
||||
## appropriate copyright notice / icon. GNU Free Documentation
|
||||
## License and Creative Commons licenses are supported so far.
|
||||
$wgRightsPage = ""; # Set to the title of a wiki page that describes your license/copyright
|
||||
$wgRightsUrl = "";
|
||||
$wgRightsText = "";
|
||||
$wgRightsIcon = "";
|
||||
|
||||
# Path to the GNU diff3 utility. Used for conflict resolution.
|
||||
$wgDiff = "${pkgs.diffutils}/bin/diff";
|
||||
$wgDiff3 = "${pkgs.diffutils}/bin/diff3";
|
||||
|
||||
# Enabled skins.
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadSkin('${k}');") cfg.skins)}
|
||||
|
||||
# Enabled extensions.
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: "wfLoadExtension('${k}');") cfg.extensions)}
|
||||
|
||||
|
||||
# End of automatically generated settings.
|
||||
# Add more configuration options below.
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
# interface
|
||||
options = {
|
||||
services.mediawiki = {
|
||||
|
||||
enable = mkEnableOption "MediaWiki";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.mediawiki;
|
||||
defaultText = literalExpression "pkgs.mediawiki";
|
||||
description = "Which MediaWiki package to use.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "MediaWiki";
|
||||
example = "Foobar Wiki";
|
||||
description = "Name of the wiki.";
|
||||
};
|
||||
|
||||
uploadsDir = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = "${stateDir}/uploads";
|
||||
description = ''
|
||||
This directory is used for uploads of pictures. The directory passed here is automatically
|
||||
created and permissions adjusted as required.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
description = "A file containing the initial password for the admin user.";
|
||||
example = "/run/keys/mediawiki-password";
|
||||
};
|
||||
|
||||
skins = mkOption {
|
||||
default = {};
|
||||
type = types.attrsOf types.path;
|
||||
description = ''
|
||||
Attribute set of paths whose content is copied to the <filename>skins</filename>
|
||||
subdirectory of the MediaWiki installation in addition to the default skins.
|
||||
'';
|
||||
};
|
||||
|
||||
extensions = mkOption {
|
||||
default = {};
|
||||
type = types.attrsOf (types.nullOr types.path);
|
||||
description = ''
|
||||
Attribute set of paths whose content is copied to the <filename>extensions</filename>
|
||||
subdirectory of the MediaWiki installation and enabled in configuration.
|
||||
|
||||
Use <literal>null</literal> instead of path to enable extensions that are part of MediaWiki.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
Matomo = pkgs.fetchzip {
|
||||
url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
|
||||
sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
|
||||
};
|
||||
ParserFunctions = null;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
|
||||
default = "mysql";
|
||||
description = "Database engine to use. MySQL/MariaDB is the database of choice by MediaWiki developers.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "mediawiki";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "mediawiki";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/mediawiki-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
tablePrefix = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
If you only have access to a single database and wish to install more than
|
||||
one version of MediaWiki, or have other applications that also use the
|
||||
database, you can give the table names a unique prefix to stop any naming
|
||||
conflicts or confusion.
|
||||
See <link xlink:href='https://www.mediawiki.org/wiki/Manual:$wgDBprefix'/>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
|
||||
defaultText = literalExpression "/run/mysqld/mysqld.sock";
|
||||
description = "Path to the unix socket file to use for authentication.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = cfg.database.type == "mysql";
|
||||
defaultText = literalExpression "true";
|
||||
description = ''
|
||||
Create the database and database user locally.
|
||||
This currently only applies if database type "mysql" is selected.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostName = "mediawiki.example.org";
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the MediaWiki PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
description = ''
|
||||
Any additional text to be appended to MediaWiki's
|
||||
LocalSettings.php configuration file. For configuration
|
||||
settings, see <link xlink:href="https://www.mediawiki.org/wiki/Manual:Configuration_settings"/>.
|
||||
'';
|
||||
default = "";
|
||||
example = ''
|
||||
$wgEnableEmail = false;
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
|
||||
message = "services.mediawiki.createLocally is currently only supported for database type 'mysql'";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
||||
message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.socket != null;
|
||||
message = "services.mediawiki.database.socket must be set if services.mediawiki.database.createLocally is set to true";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = "a password cannot be specified if services.mediawiki.database.createLocally is set to true";
|
||||
}
|
||||
];
|
||||
|
||||
services.mediawiki.skins = {
|
||||
MonoBook = "${cfg.package}/share/mediawiki/skins/MonoBook";
|
||||
Timeless = "${cfg.package}/share/mediawiki/skins/Timeless";
|
||||
Vector = "${cfg.package}/share/mediawiki/skins/Vector";
|
||||
};
|
||||
|
||||
services.mysql = mkIf cfg.database.createLocally {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.mediawiki = {
|
||||
inherit user group;
|
||||
phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
|
||||
settings = {
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
|
||||
documentRoot = mkForce "${pkg}/share/mediawiki";
|
||||
extraConfig = ''
|
||||
<Directory "${pkg}/share/mediawiki">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
|
||||
Require all granted
|
||||
DirectoryIndex index.php
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
'' + optionalString (cfg.uploadsDir != null) ''
|
||||
Alias "/images" "${cfg.uploadsDir}"
|
||||
<Directory "${cfg.uploadsDir}">
|
||||
Require all granted
|
||||
</Directory>
|
||||
'';
|
||||
} ];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${stateDir}' 0750 ${user} ${group} - -"
|
||||
"d '${cacheDir}' 0750 ${user} ${group} - -"
|
||||
] ++ optionals (cfg.uploadsDir != null) [
|
||||
"d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
|
||||
"Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
|
||||
];
|
||||
|
||||
systemd.services.mediawiki-init = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "phpfpm-mediawiki.service" ];
|
||||
after = optional cfg.database.createLocally "mysql.service";
|
||||
script = ''
|
||||
if ! test -e "${stateDir}/secret.key"; then
|
||||
tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
|
||||
fi
|
||||
|
||||
echo "exit( wfGetDB( DB_MASTER )->tableExists( 'user' ) ? 1 : 0 );" | \
|
||||
${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/eval.php --conf ${mediawikiConfig} && \
|
||||
${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
|
||||
--confpath /tmp \
|
||||
--scriptpath / \
|
||||
--dbserver ${cfg.database.host}${optionalString (cfg.database.socket != null) ":${cfg.database.socket}"} \
|
||||
--dbport ${toString cfg.database.port} \
|
||||
--dbname ${cfg.database.name} \
|
||||
${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
|
||||
--dbuser ${cfg.database.user} \
|
||||
${optionalString (cfg.database.passwordFile != null) "--dbpassfile ${cfg.database.passwordFile}"} \
|
||||
--passfile ${cfg.passwordFile} \
|
||||
${cfg.name} \
|
||||
admin
|
||||
|
||||
${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/update.php --conf ${mediawikiConfig} --quick
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
Group = group;
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.httpd.after = optional (cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service";
|
||||
|
||||
users.users.${user} = {
|
||||
group = group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ mediawikiScripts ];
|
||||
};
|
||||
}
|
||||
127
nixos/modules/services/web-apps/miniflux.nix
Normal file
127
nixos/modules/services/web-apps/miniflux.nix
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.miniflux;
|
||||
|
||||
defaultAddress = "localhost:8080";
|
||||
|
||||
dbUser = "miniflux";
|
||||
dbName = "miniflux";
|
||||
|
||||
pgbin = "${config.services.postgresql.package}/bin";
|
||||
preStart = pkgs.writeScript "miniflux-pre-start" ''
|
||||
#!${pkgs.runtimeShell}
|
||||
${pgbin}/psql "${dbName}" -c "CREATE EXTENSION IF NOT EXISTS hstore"
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.miniflux = {
|
||||
enable = mkEnableOption "miniflux and creates a local postgres database for it";
|
||||
|
||||
config = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
example = literalExpression ''
|
||||
{
|
||||
CLEANUP_FREQUENCY = "48";
|
||||
LISTEN_ADDR = "localhost:8080";
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Configuration for Miniflux, refer to
|
||||
<link xlink:href="https://miniflux.app/docs/configuration.html"/>
|
||||
for documentation on the supported values.
|
||||
|
||||
Correct configuration for the database is already provided.
|
||||
By default, listens on ${defaultAddress}.
|
||||
'';
|
||||
};
|
||||
|
||||
adminCredentialsFile = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
File containing the ADMIN_USERNAME and
|
||||
ADMIN_PASSWORD (length >= 6) in the format of
|
||||
an EnvironmentFile=, as described by systemd.exec(5).
|
||||
'';
|
||||
example = "/etc/nixos/miniflux-admin-credentials";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.miniflux.config = {
|
||||
LISTEN_ADDR = mkDefault defaultAddress;
|
||||
DATABASE_URL = "user=${dbUser} host=/run/postgresql dbname=${dbName}";
|
||||
RUN_MIGRATIONS = "1";
|
||||
CREATE_ADMIN = "1";
|
||||
};
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureUsers = [ {
|
||||
name = dbUser;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${dbName}" = "ALL PRIVILEGES";
|
||||
};
|
||||
} ];
|
||||
ensureDatabases = [ dbName ];
|
||||
};
|
||||
|
||||
systemd.services.miniflux-dbsetup = {
|
||||
description = "Miniflux database setup";
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "network.target" "postgresql.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = config.services.postgresql.superUser;
|
||||
ExecStart = preStart;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.miniflux = {
|
||||
description = "Miniflux service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ "miniflux-dbsetup.service" ];
|
||||
after = [ "network.target" "postgresql.service" "miniflux-dbsetup.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.miniflux}/bin/miniflux";
|
||||
User = dbUser;
|
||||
DynamicUser = true;
|
||||
RuntimeDirectory = "miniflux";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
EnvironmentFile = cfg.adminCredentialsFile;
|
||||
# Hardening
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DeviceAllow = [ "" ];
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
UMask = "0077";
|
||||
};
|
||||
|
||||
environment = cfg.config;
|
||||
};
|
||||
environment.systemPackages = [ pkgs.miniflux ];
|
||||
};
|
||||
}
|
||||
315
nixos/modules/services/web-apps/moodle.nix
Normal file
315
nixos/modules/services/web-apps/moodle.nix
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
|
||||
inherit (lib) concatStringsSep literalExpression mapAttrsToList optional optionalString;
|
||||
|
||||
cfg = config.services.moodle;
|
||||
fpm = config.services.phpfpm.pools.moodle;
|
||||
|
||||
user = "moodle";
|
||||
group = config.services.httpd.group;
|
||||
stateDir = "/var/lib/moodle";
|
||||
|
||||
moodleConfig = pkgs.writeText "config.php" ''
|
||||
<?php // Moodle configuration file
|
||||
|
||||
unset($CFG);
|
||||
global $CFG;
|
||||
$CFG = new stdClass();
|
||||
|
||||
$CFG->dbtype = '${ { mysql = "mariadb"; pgsql = "pgsql"; }.${cfg.database.type} }';
|
||||
$CFG->dblibrary = 'native';
|
||||
$CFG->dbhost = '${cfg.database.host}';
|
||||
$CFG->dbname = '${cfg.database.name}';
|
||||
$CFG->dbuser = '${cfg.database.user}';
|
||||
${optionalString (cfg.database.passwordFile != null) "$CFG->dbpass = file_get_contents('${cfg.database.passwordFile}');"}
|
||||
$CFG->prefix = 'mdl_';
|
||||
$CFG->dboptions = array (
|
||||
'dbpersist' => 0,
|
||||
'dbport' => '${toString cfg.database.port}',
|
||||
${optionalString (cfg.database.socket != null) "'dbsocket' => '${cfg.database.socket}',"}
|
||||
'dbcollation' => 'utf8mb4_unicode_ci',
|
||||
);
|
||||
|
||||
$CFG->wwwroot = '${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}';
|
||||
$CFG->dataroot = '${stateDir}';
|
||||
$CFG->admin = 'admin';
|
||||
|
||||
$CFG->directorypermissions = 02777;
|
||||
$CFG->disableupdateautodeploy = true;
|
||||
|
||||
$CFG->pathtogs = '${pkgs.ghostscript}/bin/gs';
|
||||
$CFG->pathtophp = '${phpExt}/bin/php';
|
||||
$CFG->pathtodu = '${pkgs.coreutils}/bin/du';
|
||||
$CFG->aspellpath = '${pkgs.aspell}/bin/aspell';
|
||||
$CFG->pathtodot = '${pkgs.graphviz}/bin/dot';
|
||||
|
||||
${cfg.extraConfig}
|
||||
|
||||
require_once('${cfg.package}/share/moodle/lib/setup.php');
|
||||
|
||||
// There is no php closing tag in this file,
|
||||
// it is intentional because it prevents trailing whitespace problems!
|
||||
'';
|
||||
|
||||
mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
|
||||
pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
|
||||
|
||||
phpExt = pkgs.php74.withExtensions
|
||||
({ enabled, all }: with all; [ iconv mbstring curl openssl tokenizer xmlrpc soap ctype zip gd simplexml dom intl json sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache ]);
|
||||
in
|
||||
{
|
||||
# interface
|
||||
options.services.moodle = {
|
||||
enable = mkEnableOption "Moodle web application";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.moodle;
|
||||
defaultText = literalExpression "pkgs.moodle";
|
||||
description = "The Moodle package to use.";
|
||||
};
|
||||
|
||||
initialPassword = mkOption {
|
||||
type = types.str;
|
||||
example = "correcthorsebatterystaple";
|
||||
description = ''
|
||||
Specifies the initial password for the admin, i.e. the password assigned if the user does not already exist.
|
||||
The password specified here is world-readable in the Nix store, so it should be changed promptly.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "mysql" "pgsql" ];
|
||||
default = "mysql";
|
||||
description = "Database engine to use.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
description = "Database host port.";
|
||||
default = {
|
||||
mysql = 3306;
|
||||
pgsql = 5432;
|
||||
}.${cfg.database.type};
|
||||
defaultText = literalExpression "3306";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "moodle";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "moodle";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/moodle-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default =
|
||||
if mysqlLocal then "/run/mysqld/mysqld.sock"
|
||||
else if pgsqlLocal then "/run/postgresql"
|
||||
else null;
|
||||
defaultText = literalExpression "/run/mysqld/mysqld.sock";
|
||||
description = "Path to the unix socket file to use for authentication.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostName = "moodle.example.org";
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the Moodle PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Any additional text to be appended to the config.php
|
||||
configuration file. This is a PHP script. For configuration
|
||||
details, see <link xlink:href="https://docs.moodle.org/37/en/Configuration_file"/>.
|
||||
'';
|
||||
example = ''
|
||||
$CFG->disableupdatenotifications = true;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
||||
message = "services.moodle.database.user must be set to ${user} if services.moodle.database.createLocally is set true";
|
||||
}
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = "a password cannot be specified if services.moodle.database.createLocally is set to true";
|
||||
}
|
||||
];
|
||||
|
||||
services.mysql = mkIf mysqlLocal {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = {
|
||||
"${cfg.database.name}.*" = "SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.postgresql = mkIf pgsqlLocal {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.moodle = {
|
||||
inherit user group;
|
||||
phpPackage = phpExt;
|
||||
phpEnv.MOODLE_CONFIG = "${moodleConfig}";
|
||||
phpOptions = ''
|
||||
zend_extension = opcache.so
|
||||
opcache.enable = 1
|
||||
'';
|
||||
settings = {
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
adminAddr = mkDefault cfg.virtualHost.adminAddr;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
|
||||
documentRoot = mkForce "${cfg.package}/share/moodle";
|
||||
extraConfig = ''
|
||||
<Directory "${cfg.package}/share/moodle">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
Options -Indexes
|
||||
DirectoryIndex index.php
|
||||
</Directory>
|
||||
'';
|
||||
} ];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${stateDir}' 0750 ${user} ${group} - -"
|
||||
];
|
||||
|
||||
systemd.services.moodle-init = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "phpfpm-moodle.service" ];
|
||||
after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
environment.MOODLE_CONFIG = moodleConfig;
|
||||
script = ''
|
||||
${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/check_database_schema.php && rc=$? || rc=$?
|
||||
|
||||
[ "$rc" == 1 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/upgrade.php \
|
||||
--non-interactive \
|
||||
--allow-unstable
|
||||
|
||||
[ "$rc" == 2 ] && ${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/install_database.php \
|
||||
--agree-license \
|
||||
--adminpass=${cfg.initialPassword}
|
||||
|
||||
true
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = user;
|
||||
Group = group;
|
||||
Type = "oneshot";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.moodle-cron = {
|
||||
description = "Moodle cron service";
|
||||
after = [ "moodle-init.service" ];
|
||||
environment.MOODLE_CONFIG = moodleConfig;
|
||||
serviceConfig = {
|
||||
User = user;
|
||||
Group = group;
|
||||
ExecStart = "${phpExt}/bin/php ${cfg.package}/share/moodle/admin/cli/cron.php";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.moodle-cron = {
|
||||
description = "Moodle cron timer";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "minutely";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.httpd.after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
|
||||
users.users.${user} = {
|
||||
group = group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
265
nixos/modules/services/web-apps/netbox.nix
Normal file
265
nixos/modules/services/web-apps/netbox.nix
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
{ config, lib, pkgs, buildEnv, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.netbox;
|
||||
staticDir = cfg.dataDir + "/static";
|
||||
configFile = pkgs.writeTextFile {
|
||||
name = "configuration.py";
|
||||
text = ''
|
||||
STATIC_ROOT = '${staticDir}'
|
||||
ALLOWED_HOSTS = ['*']
|
||||
DATABASE = {
|
||||
'NAME': 'netbox',
|
||||
'USER': 'netbox',
|
||||
'HOST': '/run/postgresql',
|
||||
}
|
||||
|
||||
# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
|
||||
# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
|
||||
# to use two separate database IDs.
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=0',
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=1',
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
||||
with open("${cfg.secretKeyFile}", "r") as file:
|
||||
SECRET_KEY = file.readline()
|
||||
|
||||
${optionalString cfg.enableLdap "REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'"}
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
};
|
||||
pkg = (pkgs.netbox.overrideAttrs (old: {
|
||||
installPhase = old.installPhase + ''
|
||||
ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
|
||||
'' + optionalString cfg.enableLdap ''
|
||||
ln -s ${ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
|
||||
'';
|
||||
})).override {
|
||||
plugins = ps: ((cfg.plugins ps)
|
||||
++ optional cfg.enableLdap [ ps.django-auth-ldap ]);
|
||||
};
|
||||
netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" ''
|
||||
#!${stdenv.shell}
|
||||
export PYTHONPATH=${pkg.pythonPath}
|
||||
sudo -u netbox ${pkg}/bin/netbox "$@"
|
||||
'');
|
||||
|
||||
in {
|
||||
options.services.netbox = {
|
||||
enable = mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable Netbox.
|
||||
|
||||
This module requires a reverse proxy that serves <literal>/static</literal> separately.
|
||||
See this <link xlink:href="https://github.com/netbox-community/netbox/blob/develop/contrib/nginx.conf/">example</link> on how to configure this.
|
||||
'';
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "[::1]";
|
||||
description = ''
|
||||
Address the server will listen on.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8001;
|
||||
description = ''
|
||||
Port the server will listen on.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.functionTo (types.listOf types.package);
|
||||
default = _: [];
|
||||
defaultText = literalExpression ''
|
||||
python3Packages: with python3Packages; [];
|
||||
'';
|
||||
description = ''
|
||||
List of plugin packages to install.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/netbox";
|
||||
description = ''
|
||||
Storage path of netbox.
|
||||
'';
|
||||
};
|
||||
|
||||
secretKeyFile = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path to a file containing the secret key.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Additional lines of configuration appended to the <literal>configuration.py</literal>.
|
||||
See the <link xlink:href="https://netbox.readthedocs.io/en/stable/configuration/optional-settings/">documentation</link> for more possible options.
|
||||
'';
|
||||
};
|
||||
|
||||
enableLdap = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable LDAP-Authentication for Netbox.
|
||||
|
||||
This requires a configuration file being pass through <literal>ldapConfigPath</literal>.
|
||||
'';
|
||||
};
|
||||
|
||||
ldapConfigPath = mkOption {
|
||||
type = types.path;
|
||||
default = "";
|
||||
description = ''
|
||||
Path to the Configuration-File for LDAP-Authentification, will be loaded as <literal>ldap_config.py</literal>.
|
||||
See the <link xlink:href="https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration">documentation</link> for possible options.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.redis.servers.netbox.enable = true;
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureDatabases = [ "netbox" ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "netbox";
|
||||
ensurePermissions = {
|
||||
"DATABASE netbox" = "ALL PRIVILEGES";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
environment.systemPackages = [ netboxManageScript ];
|
||||
|
||||
systemd.targets.netbox = {
|
||||
description = "Target for all NetBox services";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" "redis-netbox.service" ];
|
||||
};
|
||||
|
||||
systemd.services = let
|
||||
defaultServiceConfig = {
|
||||
WorkingDirectory = "${cfg.dataDir}";
|
||||
User = "netbox";
|
||||
Group = "netbox";
|
||||
StateDirectory = "netbox";
|
||||
StateDirectoryMode = "0750";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
in {
|
||||
netbox-migration = {
|
||||
description = "NetBox migrations";
|
||||
wantedBy = [ "netbox.target" ];
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = pkg.pythonPath;
|
||||
};
|
||||
|
||||
serviceConfig = defaultServiceConfig // {
|
||||
Type = "oneshot";
|
||||
ExecStart = ''
|
||||
${pkg}/bin/netbox migrate
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
netbox = {
|
||||
description = "NetBox WSGI Service";
|
||||
wantedBy = [ "netbox.target" ];
|
||||
after = [ "netbox-migration.service" ];
|
||||
|
||||
preStart = ''
|
||||
${pkg}/bin/netbox trace_paths --no-input
|
||||
${pkg}/bin/netbox collectstatic --no-input
|
||||
${pkg}/bin/netbox remove_stale_contenttypes --no-input
|
||||
'';
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = pkg.pythonPath;
|
||||
};
|
||||
|
||||
serviceConfig = defaultServiceConfig // {
|
||||
ExecStart = ''
|
||||
${pkgs.python3Packages.gunicorn}/bin/gunicorn netbox.wsgi \
|
||||
--bind ${cfg.listenAddress}:${toString cfg.port} \
|
||||
--pythonpath ${pkg}/opt/netbox/netbox
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
netbox-rq = {
|
||||
description = "NetBox Request Queue Worker";
|
||||
wantedBy = [ "netbox.target" ];
|
||||
after = [ "netbox.service" ];
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = pkg.pythonPath;
|
||||
};
|
||||
|
||||
serviceConfig = defaultServiceConfig // {
|
||||
ExecStart = ''
|
||||
${pkg}/bin/netbox rqworker high default low
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
netbox-housekeeping = {
|
||||
description = "NetBox housekeeping job";
|
||||
after = [ "netbox.service" ];
|
||||
|
||||
environment = {
|
||||
PYTHONPATH = pkg.pythonPath;
|
||||
};
|
||||
|
||||
serviceConfig = defaultServiceConfig // {
|
||||
Type = "oneshot";
|
||||
ExecStart = ''
|
||||
${pkg}/bin/netbox housekeeping
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.netbox-housekeeping = {
|
||||
description = "Run NetBox housekeeping job";
|
||||
wantedBy = [ "timers.target" ];
|
||||
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.netbox = {
|
||||
home = "${cfg.dataDir}";
|
||||
isSystemUser = true;
|
||||
group = "netbox";
|
||||
};
|
||||
users.groups.netbox = {};
|
||||
users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
|
||||
};
|
||||
}
|
||||
1029
nixos/modules/services/web-apps/nextcloud.nix
Normal file
1029
nixos/modules/services/web-apps/nextcloud.nix
Normal file
File diff suppressed because it is too large
Load diff
291
nixos/modules/services/web-apps/nextcloud.xml
Normal file
291
nixos/modules/services/web-apps/nextcloud.xml
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-nextcloud">
|
||||
<title>Nextcloud</title>
|
||||
<para>
|
||||
<link xlink:href="https://nextcloud.com/">Nextcloud</link> is an open-source,
|
||||
self-hostable cloud platform. The server setup can be automated using
|
||||
<link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
|
||||
desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
|
||||
</para>
|
||||
<para>
|
||||
The current default by NixOS is <package>nextcloud24</package> which is also the latest
|
||||
major version available.
|
||||
</para>
|
||||
<section xml:id="module-services-nextcloud-basic-usage">
|
||||
<title>Basic usage</title>
|
||||
|
||||
<para>
|
||||
Nextcloud is a PHP-based application which requires an HTTP server
|
||||
(<literal><link linkend="opt-services.nextcloud.enable">services.nextcloud</link></literal>
|
||||
optionally supports
|
||||
<literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>)
|
||||
and a database (it's recommended to use
|
||||
<literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>).
|
||||
</para>
|
||||
|
||||
<para>
|
||||
A very basic configuration may look like this:
|
||||
<programlisting>{ pkgs, ... }:
|
||||
{
|
||||
services.nextcloud = {
|
||||
<link linkend="opt-services.nextcloud.enable">enable</link> = true;
|
||||
<link linkend="opt-services.nextcloud.hostName">hostName</link> = "nextcloud.tld";
|
||||
config = {
|
||||
<link linkend="opt-services.nextcloud.config.dbtype">dbtype</link> = "pgsql";
|
||||
<link linkend="opt-services.nextcloud.config.dbuser">dbuser</link> = "nextcloud";
|
||||
<link linkend="opt-services.nextcloud.config.dbhost">dbhost</link> = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
|
||||
<link linkend="opt-services.nextcloud.config.dbname">dbname</link> = "nextcloud";
|
||||
<link linkend="opt-services.nextcloud.config.adminpassFile">adminpassFile</link> = "/path/to/admin-pass-file";
|
||||
<link linkend="opt-services.nextcloud.config.adminuser">adminuser</link> = "root";
|
||||
};
|
||||
};
|
||||
|
||||
services.postgresql = {
|
||||
<link linkend="opt-services.postgresql.enable">enable</link> = true;
|
||||
<link linkend="opt-services.postgresql.ensureDatabases">ensureDatabases</link> = [ "nextcloud" ];
|
||||
<link linkend="opt-services.postgresql.ensureUsers">ensureUsers</link> = [
|
||||
{ name = "nextcloud";
|
||||
ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# ensure that postgres is running *before* running the setup
|
||||
systemd.services."nextcloud-setup" = {
|
||||
requires = ["postgresql.service"];
|
||||
after = ["postgresql.service"];
|
||||
};
|
||||
|
||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
||||
}</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
The <literal>hostName</literal> option is used internally to configure an HTTP
|
||||
server using <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
|
||||
and <literal>nginx</literal>. The <literal>config</literal> attribute set is
|
||||
used by the imperative installer and all values are written to an additional file
|
||||
to ensure that changes can be applied by changing the module's options.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
In case the application serves multiple domains (those are checked with
|
||||
<literal><link xlink:href="http://php.net/manual/en/reserved.variables.server.php">$_SERVER['HTTP_HOST']</link></literal>)
|
||||
it's needed to add them to
|
||||
<literal><link linkend="opt-services.nextcloud.config.extraTrustedDomains">services.nextcloud.config.extraTrustedDomains</link></literal>.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Auto updates for Nextcloud apps can be enabled using
|
||||
<literal><link linkend="opt-services.nextcloud.autoUpdateApps.enable">services.nextcloud.autoUpdateApps</link></literal>.
|
||||
</para>
|
||||
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-nextcloud-pitfalls-during-upgrade">
|
||||
<title>Common problems</title>
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<formalpara>
|
||||
<title>General notes</title>
|
||||
<para>
|
||||
Unfortunately Nextcloud appears to be very stateful when it comes to
|
||||
managing its own configuration. The config file lives in the home directory
|
||||
of the <literal>nextcloud</literal> user (by default
|
||||
<literal>/var/lib/nextcloud/config/config.php</literal>) and is also used to
|
||||
track several states of the application (e.g., whether installed or not).
|
||||
</para>
|
||||
</formalpara>
|
||||
<para>
|
||||
All configuration parameters are also stored in
|
||||
<filename>/var/lib/nextcloud/config/override.config.php</filename> which is generated by
|
||||
the module and linked from the store to ensure that all values from
|
||||
<filename>config.php</filename> can be modified by the module.
|
||||
However <filename>config.php</filename> manages the application's state and shouldn't be
|
||||
touched manually because of that.
|
||||
</para>
|
||||
<warning>
|
||||
<para>Don't delete <filename>config.php</filename>! This file
|
||||
tracks the application's state and a deletion can cause unwanted
|
||||
side-effects!</para>
|
||||
</warning>
|
||||
|
||||
<warning>
|
||||
<para>Don't rerun <literal>nextcloud-occ
|
||||
maintenance:install</literal>! This command tries to install the application
|
||||
and can cause unwanted side-effects!</para>
|
||||
</warning>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<formalpara>
|
||||
<title>Multiple version upgrades</title>
|
||||
<para>
|
||||
Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
|
||||
<literal>v16</literal>, you cannot upgrade to <literal>v18</literal>, you need to upgrade to
|
||||
<literal>v17</literal> first. This is ensured automatically as long as the
|
||||
<link linkend="opt-system.stateVersion">stateVersion</link> is declared properly. In that case
|
||||
the oldest version available (one major behind the one from the previous NixOS
|
||||
release) will be selected by default and the module will generate a warning that reminds
|
||||
the user to upgrade to latest Nextcloud <emphasis>after</emphasis> that deploy.
|
||||
</para>
|
||||
</formalpara>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<formalpara>
|
||||
<title><literal>Error: Command "upgrade" is not defined.</literal></title>
|
||||
<para>
|
||||
This error usually occurs if the initial installation
|
||||
(<command>nextcloud-occ maintenance:install</command>) has failed. After that, the application
|
||||
is not installed, but the upgrade is attempted to be executed. Further context can
|
||||
be found in <link xlink:href="https://github.com/NixOS/nixpkgs/issues/111175">NixOS/nixpkgs#111175</link>.
|
||||
</para>
|
||||
</formalpara>
|
||||
<para>
|
||||
First of all, it makes sense to find out what went wrong by looking at the logs
|
||||
of the installation via <command>journalctl -u nextcloud-setup</command> and try to fix
|
||||
the underlying issue.
|
||||
</para>
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
If this occurs on an <emphasis>existing</emphasis> setup, this is most likely because
|
||||
the maintenance mode is active. It can be deactivated by running
|
||||
<command>nextcloud-occ maintenance:mode --off</command>. It's advisable though to
|
||||
check the logs first on why the maintenance mode was activated.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<warning><para>Only perform the following measures on
|
||||
<emphasis>freshly installed instances!</emphasis></para></warning>
|
||||
<para>
|
||||
A re-run of the installer can be forced by <emphasis>deleting</emphasis>
|
||||
<filename>/var/lib/nextcloud/config/config.php</filename>. This is the only time
|
||||
advisable because the fresh install doesn't have any state that can be lost.
|
||||
In case that doesn't help, an entire re-creation can be forced via
|
||||
<command>rm -rf ~nextcloud/</command>.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-nextcloud-httpd">
|
||||
<title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
|
||||
<para>
|
||||
By default, <package>nginx</package> is used as reverse-proxy for <package>nextcloud</package>.
|
||||
However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
|
||||
<package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
|
||||
settings <literal>listen.owner</literal> & <literal>listen.group</literal> in the
|
||||
<link linkend="opt-services.phpfpm.pools">corresponding <literal>phpfpm</literal> pool</link>.
|
||||
</para>
|
||||
<para>
|
||||
An exemplary configuration may look like this:
|
||||
<programlisting>{ config, lib, pkgs, ... }: {
|
||||
<link linkend="opt-services.nginx.enable">services.nginx.enable</link> = false;
|
||||
services.nextcloud = {
|
||||
<link linkend="opt-services.nextcloud.enable">enable</link> = true;
|
||||
<link linkend="opt-services.nextcloud.hostName">hostName</link> = "localhost";
|
||||
|
||||
/* further, required options */
|
||||
};
|
||||
<link linkend="opt-services.phpfpm.pools._name_.settings">services.phpfpm.pools.nextcloud.settings</link> = {
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
};
|
||||
services.httpd = {
|
||||
<link linkend="opt-services.httpd.enable">enable</link> = true;
|
||||
<link linkend="opt-services.httpd.adminAddr">adminAddr</link> = "webmaster@localhost";
|
||||
<link linkend="opt-services.httpd.extraModules">extraModules</link> = [ "proxy_fcgi" ];
|
||||
virtualHosts."localhost" = {
|
||||
<link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = config.services.nextcloud.package;
|
||||
<link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
|
||||
<Directory "${config.services.nextcloud.package}">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]
|
||||
</IfModule>
|
||||
DirectoryIndex index.php
|
||||
Require all granted
|
||||
Options +FollowSymLinks
|
||||
</Directory>
|
||||
'';
|
||||
};
|
||||
};
|
||||
}</programlisting>
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="installing-apps-php-extensions-nextcloud">
|
||||
<title>Installing Apps and PHP extensions</title>
|
||||
|
||||
<para>
|
||||
Nextcloud apps are installed statefully through the web interface.
|
||||
|
||||
Some apps may require extra PHP extensions to be installed.
|
||||
This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Alternatively, extra apps can also be declared with the <xref linkend="opt-services.nextcloud.extraApps" /> setting.
|
||||
When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
|
||||
that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section xml:id="module-services-nextcloud-maintainer-info">
|
||||
<title>Maintainer information</title>
|
||||
|
||||
<para>
|
||||
As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
|
||||
since it cannot move more than one major version forward on a single upgrade. This chapter
|
||||
adds some notes how Nextcloud updates should be rolled out in the future.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
While minor and patch-level updates are no problem and can be done directly in the
|
||||
package-expression (and should be backported to supported stable branches after that),
|
||||
major-releases should be added in a new attribute (e.g. Nextcloud <literal>v19.0.0</literal>
|
||||
should be available in <literal>nixpkgs</literal> as <literal>pkgs.nextcloud19</literal>).
|
||||
To provide simple upgrade paths it's generally useful to backport those as well to stable
|
||||
branches. As long as the package-default isn't altered, this won't break existing setups.
|
||||
After that, the versioning-warning in the <literal>nextcloud</literal>-module should be
|
||||
updated to make sure that the
|
||||
<link linkend="opt-services.nextcloud.package">package</link>-option selects the latest version
|
||||
on fresh setups.
|
||||
</para>
|
||||
|
||||
<para>
|
||||
If major-releases will be abandoned by upstream, we should check first if those are needed
|
||||
in NixOS for a safe upgrade-path before removing those. In that case we shold keep those
|
||||
packages, but mark them as insecure in an expression like this (in
|
||||
<literal><nixpkgs/pkgs/servers/nextcloud/default.nix></literal>):
|
||||
<programlisting>/* ... */
|
||||
{
|
||||
nextcloud17 = generic {
|
||||
version = "17.0.x";
|
||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||
eol = true;
|
||||
};
|
||||
}</programlisting>
|
||||
</para>
|
||||
|
||||
<para>
|
||||
Ideally we should make sure that it's possible to jump two NixOS versions forward:
|
||||
i.e. the warnings and the logic in the module should guard a user to upgrade from a
|
||||
Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
156
nixos/modules/services/web-apps/nexus.nix
Normal file
156
nixos/modules/services/web-apps/nexus.nix
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.nexus;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.nexus = {
|
||||
enable = mkEnableOption "Sonatype Nexus3 OSS service";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.nexus;
|
||||
defaultText = literalExpression "pkgs.nexus";
|
||||
description = "Package which runs Nexus3";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "nexus";
|
||||
description = "User which runs Nexus3.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "nexus";
|
||||
description = "Group which runs Nexus3.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/sonatype-work";
|
||||
description = "Home directory of the Nexus3 instance.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Address to listen on.";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 8081;
|
||||
description = "Port to listen on.";
|
||||
};
|
||||
|
||||
jvmOpts = mkOption {
|
||||
type = types.lines;
|
||||
default = ''
|
||||
-Xms1200M
|
||||
-Xmx1200M
|
||||
-XX:MaxDirectMemorySize=2G
|
||||
-XX:+UnlockDiagnosticVMOptions
|
||||
-XX:+UnsyncloadClass
|
||||
-XX:+LogVMOutput
|
||||
-XX:LogFile=${cfg.home}/nexus3/log/jvm.log
|
||||
-XX:-OmitStackTraceInFastThrow
|
||||
-Djava.net.preferIPv4Stack=true
|
||||
-Dkaraf.home=${cfg.package}
|
||||
-Dkaraf.base=${cfg.package}
|
||||
-Dkaraf.etc=${cfg.package}/etc/karaf
|
||||
-Djava.util.logging.config.file=${cfg.package}/etc/karaf/java.util.logging.properties
|
||||
-Dkaraf.data=${cfg.home}/nexus3
|
||||
-Djava.io.tmpdir=${cfg.home}/nexus3/tmp
|
||||
-Dkaraf.startLocalConsole=false
|
||||
-Djava.endorsed.dirs=${cfg.package}/lib/endorsed
|
||||
'';
|
||||
defaultText = literalExpression ''
|
||||
'''
|
||||
-Xms1200M
|
||||
-Xmx1200M
|
||||
-XX:MaxDirectMemorySize=2G
|
||||
-XX:+UnlockDiagnosticVMOptions
|
||||
-XX:+UnsyncloadClass
|
||||
-XX:+LogVMOutput
|
||||
-XX:LogFile=''${home}/nexus3/log/jvm.log
|
||||
-XX:-OmitStackTraceInFastThrow
|
||||
-Djava.net.preferIPv4Stack=true
|
||||
-Dkaraf.home=''${package}
|
||||
-Dkaraf.base=''${package}
|
||||
-Dkaraf.etc=''${package}/etc/karaf
|
||||
-Djava.util.logging.config.file=''${package}/etc/karaf/java.util.logging.properties
|
||||
-Dkaraf.data=''${home}/nexus3
|
||||
-Djava.io.tmpdir=''${home}/nexus3/tmp
|
||||
-Dkaraf.startLocalConsole=false
|
||||
-Djava.endorsed.dirs=''${package}/lib/endorsed
|
||||
'''
|
||||
'';
|
||||
|
||||
description = ''
|
||||
Options for the JVM written to `nexus.jvmopts`.
|
||||
Please refer to the docs (https://help.sonatype.com/repomanager3/installation/configuring-the-runtime-environment)
|
||||
for further information.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.home;
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
systemd.services.nexus = {
|
||||
description = "Sonatype Nexus3";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ cfg.home ];
|
||||
|
||||
environment = {
|
||||
NEXUS_USER = cfg.user;
|
||||
NEXUS_HOME = cfg.home;
|
||||
|
||||
VM_OPTS_FILE = pkgs.writeText "nexus.vmoptions" cfg.jvmOpts;
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
mkdir -p ${cfg.home}/nexus3/etc
|
||||
|
||||
if [ ! -f ${cfg.home}/nexus3/etc/nexus.properties ]; then
|
||||
echo "# Jetty section" > ${cfg.home}/nexus3/etc/nexus.properties
|
||||
echo "application-port=${toString cfg.listenPort}" >> ${cfg.home}/nexus3/etc/nexus.properties
|
||||
echo "application-host=${toString cfg.listenAddress}" >> ${cfg.home}/nexus3/etc/nexus.properties
|
||||
else
|
||||
sed 's/^application-port=.*/application-port=${toString cfg.listenPort}/' -i ${cfg.home}/nexus3/etc/nexus.properties
|
||||
sed 's/^# application-port=.*/application-port=${toString cfg.listenPort}/' -i ${cfg.home}/nexus3/etc/nexus.properties
|
||||
sed 's/^application-host=.*/application-host=${toString cfg.listenAddress}/' -i ${cfg.home}/nexus3/etc/nexus.properties
|
||||
sed 's/^# application-host=.*/application-host=${toString cfg.listenAddress}/' -i ${cfg.home}/nexus3/etc/nexus.properties
|
||||
fi
|
||||
'';
|
||||
|
||||
script = "${cfg.package}/bin/nexus run";
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PrivateTmp = true;
|
||||
LimitNOFILE = 102642;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ ironpinguin ];
|
||||
}
|
||||
318
nixos/modules/services/web-apps/nifi.nix
Normal file
318
nixos/modules/services/web-apps/nifi.nix
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
{ lib, pkgs, config, options, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.nifi;
|
||||
opt = options.services.nifi;
|
||||
|
||||
env = {
|
||||
NIFI_OVERRIDE_NIFIENV = "true";
|
||||
NIFI_HOME = "/var/lib/nifi";
|
||||
NIFI_PID_DIR = "/run/nifi";
|
||||
NIFI_LOG_DIR = "/var/log/nifi";
|
||||
};
|
||||
|
||||
envFile = pkgs.writeText "nifi.env" (lib.concatMapStrings (s: s + "\n") (
|
||||
(lib.concatLists (lib.mapAttrsToList (name: value:
|
||||
if value != null then [
|
||||
"${name}=\"${toString value}\""
|
||||
] else []
|
||||
) env))));
|
||||
|
||||
nifiEnv = pkgs.writeShellScriptBin "nifi-env" ''
|
||||
set -a
|
||||
source "${envFile}"
|
||||
eval -- "\$@"
|
||||
'';
|
||||
|
||||
in {
|
||||
options = {
|
||||
services.nifi = {
|
||||
enable = lib.mkEnableOption "Apache NiFi";
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.nifi;
|
||||
defaultText = lib.literalExpression "pkgs.nifi";
|
||||
description = "Apache NiFi package to use.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nifi";
|
||||
description = "User account where Apache NiFi runs.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nifi";
|
||||
description = "Group account where Apache NiFi runs.";
|
||||
};
|
||||
|
||||
enableHTTPS = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Enable HTTPS protocol. Don`t use in production.";
|
||||
};
|
||||
|
||||
listenHost = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if cfg.enableHTTPS then "0.0.0.0" else "127.0.0.1";
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.enableHTTPS}
|
||||
then "0.0.0.0"
|
||||
else "127.0.0.1"
|
||||
'';
|
||||
description = "Bind to an ip for Apache NiFi web-ui.";
|
||||
};
|
||||
|
||||
listenPort = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = if cfg.enableHTTPS then 8443 else 8080;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.enableHTTPS}
|
||||
then "8443"
|
||||
else "8000"
|
||||
'';
|
||||
description = "Bind to a port for Apache NiFi web-ui.";
|
||||
};
|
||||
|
||||
proxyHost = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if cfg.enableHTTPS then "0.0.0.0" else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.enableHTTPS}
|
||||
then "0.0.0.0"
|
||||
else null
|
||||
'';
|
||||
description = "Allow requests from a specific host.";
|
||||
};
|
||||
|
||||
proxyPort = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = if cfg.enableHTTPS then 8443 else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.enableHTTPS}
|
||||
then "8443"
|
||||
else null
|
||||
'';
|
||||
description = "Allow requests from a specific port.";
|
||||
};
|
||||
|
||||
initUser = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Initial user account for Apache NiFi. Username must be at least 4 characters.";
|
||||
};
|
||||
|
||||
initPasswordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/nifi/password-nifi";
|
||||
description = "nitial password for Apache NiFi. Password must be at least 12 characters.";
|
||||
};
|
||||
|
||||
initJavaHeapSize = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
example = 1024;
|
||||
description = "Set the initial heap size for the JVM in MB.";
|
||||
};
|
||||
|
||||
maxJavaHeapSize = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
example = 2048;
|
||||
description = "Set the initial heap size for the JVM in MB.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = cfg.initUser!=null || cfg.initPasswordFile==null;
|
||||
message = ''
|
||||
<option>services.nifi.initUser</option> needs to be set if <option>services.nifi.initPasswordFile</option> enabled.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.initUser==null || cfg.initPasswordFile!=null;
|
||||
message = ''
|
||||
<option>services.nifi.initPasswordFile</option> needs to be set if <option>services.nifi.initUser</option> enabled.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.proxyHost==null || cfg.proxyPort!=null;
|
||||
message = ''
|
||||
<option>services.nifi.proxyPort</option> needs to be set if <option>services.nifi.proxyHost</option> value specified.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.proxyHost!=null || cfg.proxyPort==null;
|
||||
message = ''
|
||||
<option>services.nifi.proxyHost</option> needs to be set if <option>services.nifi.proxyPort</option> value specified.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.initJavaHeapSize==null || cfg.maxJavaHeapSize!=null;
|
||||
message = ''
|
||||
<option>services.nifi.maxJavaHeapSize</option> needs to be set if <option>services.nifi.initJavaHeapSize</option> value specified.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.initJavaHeapSize!=null || cfg.maxJavaHeapSize==null;
|
||||
message = ''
|
||||
<option>services.nifi.initJavaHeapSize</option> needs to be set if <option>services.nifi.maxJavaHeapSize</option> value specified.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
warnings = lib.optional (cfg.enableHTTPS==false) ''
|
||||
Please do not disable HTTPS mode in production. In this mode, access to the nifi is opened without authentication.
|
||||
'';
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '/var/lib/nifi/conf' 0750 ${cfg.user} ${cfg.group}"
|
||||
"L+ '/var/lib/nifi/lib' - - - - ${cfg.package}/lib"
|
||||
];
|
||||
|
||||
|
||||
systemd.services.nifi = {
|
||||
description = "Apache NiFi";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = env;
|
||||
path = [ pkgs.gawk ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
PIDFile = "/run/nifi/nifi.pid";
|
||||
ExecStartPre = pkgs.writeScript "nifi-pre-start.sh" ''
|
||||
#!/bin/sh
|
||||
umask 077
|
||||
test -f '/var/lib/nifi/conf/authorizers.xml' || (cp '${cfg.package}/share/nifi/conf/authorizers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/authorizers.xml')
|
||||
test -f '/var/lib/nifi/conf/bootstrap.conf' || (cp '${cfg.package}/share/nifi/conf/bootstrap.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap.conf')
|
||||
test -f '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf' || (cp '${cfg.package}/share/nifi/conf/bootstrap-hashicorp-vault.conf' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-hashicorp-vault.conf')
|
||||
test -f '/var/lib/nifi/conf/bootstrap-notification-services.xml' || (cp '${cfg.package}/share/nifi/conf/bootstrap-notification-services.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/bootstrap-notification-services.xml')
|
||||
test -f '/var/lib/nifi/conf/logback.xml' || (cp '${cfg.package}/share/nifi/conf/logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/logback.xml')
|
||||
test -f '/var/lib/nifi/conf/login-identity-providers.xml' || (cp '${cfg.package}/share/nifi/conf/login-identity-providers.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/login-identity-providers.xml')
|
||||
test -f '/var/lib/nifi/conf/nifi.properties' || (cp '${cfg.package}/share/nifi/conf/nifi.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/nifi.properties')
|
||||
test -f '/var/lib/nifi/conf/stateless-logback.xml' || (cp '${cfg.package}/share/nifi/conf/stateless-logback.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless-logback.xml')
|
||||
test -f '/var/lib/nifi/conf/stateless.properties' || (cp '${cfg.package}/share/nifi/conf/stateless.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/stateless.properties')
|
||||
test -f '/var/lib/nifi/conf/state-management.xml' || (cp '${cfg.package}/share/nifi/conf/state-management.xml' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/state-management.xml')
|
||||
test -f '/var/lib/nifi/conf/zookeeper.properties' || (cp '${cfg.package}/share/nifi/conf/zookeeper.properties' '/var/lib/nifi/conf/' && chmod 0640 '/var/lib/nifi/conf/zookeeper.properties')
|
||||
test -d '/var/lib/nifi/docs/html' || (mkdir -p /var/lib/nifi/docs && cp -r '${cfg.package}/share/nifi/docs/html' '/var/lib/nifi/docs/html')
|
||||
${lib.optionalString ((cfg.initUser != null) && (cfg.initPasswordFile != null)) ''
|
||||
awk -F'[<|>]' '/property name="Username"/ {if ($3!="") f=1} END{exit !f}' /var/lib/nifi/conf/login-identity-providers.xml || ${cfg.package}/bin/nifi.sh set-single-user-credentials ${cfg.initUser} $(cat ${cfg.initPasswordFile})
|
||||
''}
|
||||
${lib.optionalString (cfg.enableHTTPS == false) ''
|
||||
sed -i /var/lib/nifi/conf/nifi.properties \
|
||||
-e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=false|g' \
|
||||
-e 's|nifi.web.http.host=.*|nifi.web.http.host=${cfg.listenHost}|g' \
|
||||
-e 's|nifi.web.http.port=.*|nifi.web.http.port=${(toString cfg.listenPort)}|g' \
|
||||
-e 's|nifi.web.https.host=.*|nifi.web.https.host=|g' \
|
||||
-e 's|nifi.web.https.port=.*|nifi.web.https.port=|g' \
|
||||
-e 's|nifi.security.keystore=.*|nifi.security.keystore=|g' \
|
||||
-e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=|g' \
|
||||
-e 's|nifi.security.truststore=.*|nifi.security.truststore=|g' \
|
||||
-e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=|g' \
|
||||
-e '/nifi.security.keystorePasswd/s|^|#|' \
|
||||
-e '/nifi.security.keyPasswd/s|^|#|' \
|
||||
-e '/nifi.security.truststorePasswd/s|^|#|'
|
||||
''}
|
||||
${lib.optionalString (cfg.enableHTTPS == true) ''
|
||||
sed -i /var/lib/nifi/conf/nifi.properties \
|
||||
-e 's|nifi.remote.input.secure=.*|nifi.remote.input.secure=true|g' \
|
||||
-e 's|nifi.web.http.host=.*|nifi.web.http.host=|g' \
|
||||
-e 's|nifi.web.http.port=.*|nifi.web.http.port=|g' \
|
||||
-e 's|nifi.web.https.host=.*|nifi.web.https.host=${cfg.listenHost}|g' \
|
||||
-e 's|nifi.web.https.port=.*|nifi.web.https.port=${(toString cfg.listenPort)}|g' \
|
||||
-e 's|nifi.security.keystore=.*|nifi.security.keystore=./conf/keystore.p12|g' \
|
||||
-e 's|nifi.security.keystoreType=.*|nifi.security.keystoreType=PKCS12|g' \
|
||||
-e 's|nifi.security.truststore=.*|nifi.security.truststore=./conf/truststore.p12|g' \
|
||||
-e 's|nifi.security.truststoreType=.*|nifi.security.truststoreType=PKCS12|g' \
|
||||
-e '/nifi.security.keystorePasswd/s|^#\+||' \
|
||||
-e '/nifi.security.keyPasswd/s|^#\+||' \
|
||||
-e '/nifi.security.truststorePasswd/s|^#\+||'
|
||||
''}
|
||||
${lib.optionalString ((cfg.enableHTTPS == true) && (cfg.proxyHost != null) && (cfg.proxyPort != null)) ''
|
||||
sed -i /var/lib/nifi/conf/nifi.properties \
|
||||
-e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=${cfg.proxyHost}:${(toString cfg.proxyPort)}|g'
|
||||
''}
|
||||
${lib.optionalString ((cfg.enableHTTPS == false) || (cfg.proxyHost == null) && (cfg.proxyPort == null)) ''
|
||||
sed -i /var/lib/nifi/conf/nifi.properties \
|
||||
-e 's|nifi.web.proxy.host=.*|nifi.web.proxy.host=|g'
|
||||
''}
|
||||
${lib.optionalString ((cfg.initJavaHeapSize != null) && (cfg.maxJavaHeapSize != null))''
|
||||
sed -i /var/lib/nifi/conf/bootstrap.conf \
|
||||
-e 's|java.arg.2=.*|java.arg.2=-Xms${(toString cfg.initJavaHeapSize)}m|g' \
|
||||
-e 's|java.arg.3=.*|java.arg.3=-Xmx${(toString cfg.maxJavaHeapSize)}m|g'
|
||||
''}
|
||||
${lib.optionalString ((cfg.initJavaHeapSize == null) && (cfg.maxJavaHeapSize == null))''
|
||||
sed -i /var/lib/nifi/conf/bootstrap.conf \
|
||||
-e 's|java.arg.2=.*|java.arg.2=-Xms512m|g' \
|
||||
-e 's|java.arg.3=.*|java.arg.3=-Xmx512m|g'
|
||||
''}
|
||||
'';
|
||||
ExecStart = "${cfg.package}/bin/nifi.sh start";
|
||||
ExecStop = "${cfg.package}/bin/nifi.sh stop";
|
||||
# User and group
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
# Runtime directory and mode
|
||||
RuntimeDirectory = "nifi";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
# State directory and mode
|
||||
StateDirectory = "nifi";
|
||||
StateDirectoryMode = "0750";
|
||||
# Logs directory and mode
|
||||
LogsDirectory = "nifi";
|
||||
LogsDirectoryMode = "0750";
|
||||
# Proc filesystem
|
||||
ProcSubset = "pid";
|
||||
ProtectProc = "invisible";
|
||||
# Access write directories
|
||||
ReadWritePaths = [ cfg.initPasswordFile ];
|
||||
UMask = "0027";
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "";
|
||||
# Security
|
||||
NoNewPrivileges = true;
|
||||
# Sandboxing
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
PrivateIPC = true;
|
||||
PrivateUsers = true;
|
||||
ProtectHostname = true;
|
||||
ProtectClock = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = [ "AF_INET AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = false;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RemoveIPC = true;
|
||||
PrivateMounts = true;
|
||||
# System Call Filtering
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @resources @privileged @setuid" "@chown" ];
|
||||
};
|
||||
};
|
||||
|
||||
users.users = lib.mkMerge [
|
||||
(lib.mkIf (cfg.user == "nifi") {
|
||||
nifi = {
|
||||
group = cfg.group;
|
||||
isSystemUser = true;
|
||||
home = cfg.package;
|
||||
};
|
||||
})
|
||||
(lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package nifiEnv ])
|
||||
];
|
||||
|
||||
users.groups = lib.optionalAttrs (cfg.group == "nifi") {
|
||||
nifi = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
149
nixos/modules/services/web-apps/node-red.nix
Normal file
149
nixos/modules/services/web-apps/node-red.nix
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.node-red;
|
||||
defaultUser = "node-red";
|
||||
finalPackage = if cfg.withNpmAndGcc then node-red_withNpmAndGcc else cfg.package;
|
||||
node-red_withNpmAndGcc = pkgs.runCommand "node-red" {
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
makeWrapper ${pkgs.nodePackages.node-red}/bin/node-red $out/bin/node-red \
|
||||
--set PATH '${lib.makeBinPath [ pkgs.nodePackages.npm pkgs.gcc ]}:$PATH' \
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.node-red = {
|
||||
enable = mkEnableOption "the Node-RED service";
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.nodePackages.node-red;
|
||||
defaultText = literalExpression "pkgs.nodePackages.node-red";
|
||||
type = types.package;
|
||||
description = "Node-RED package to use.";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Open ports in the firewall for the server.
|
||||
'';
|
||||
};
|
||||
|
||||
withNpmAndGcc = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Give Node-RED access to NPM and GCC at runtime, so 'Nodes' can be
|
||||
downloaded and managed imperatively via the 'Palette Manager'.
|
||||
'';
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.path;
|
||||
default = "${cfg.package}/lib/node_modules/node-red/settings.js";
|
||||
defaultText = literalExpression ''"''${package}/lib/node_modules/node-red/settings.js"'';
|
||||
description = ''
|
||||
Path to the JavaScript configuration file.
|
||||
See <link
|
||||
xlink:href="https://github.com/node-red/node-red/blob/master/packages/node_modules/node-red/settings.js"/>
|
||||
for a configuration example.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 1880;
|
||||
description = "Listening port.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = defaultUser;
|
||||
description = ''
|
||||
User under which Node-RED runs.If left as the default value this user
|
||||
will automatically be created on system activation, otherwise the
|
||||
sysadmin is responsible for ensuring the user exists.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = defaultUser;
|
||||
description = ''
|
||||
Group under which Node-RED runs.If left as the default value this group
|
||||
will automatically be created on system activation, otherwise the
|
||||
sysadmin is responsible for ensuring the group exists.
|
||||
'';
|
||||
};
|
||||
|
||||
userDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/node-red";
|
||||
description = ''
|
||||
The directory to store all user data, such as flow and credential files and all library data. If left
|
||||
as the default value this directory will automatically be created before the node-red service starts,
|
||||
otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership
|
||||
and permissions.
|
||||
'';
|
||||
};
|
||||
|
||||
safe = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to launch Node-RED in --safe mode.";
|
||||
};
|
||||
|
||||
define = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = "List of settings.js overrides to pass via -D to Node-RED.";
|
||||
example = literalExpression ''
|
||||
{
|
||||
"logging.console.level" = "trace";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users = optionalAttrs (cfg.user == defaultUser) {
|
||||
${defaultUser} = {
|
||||
isSystemUser = true;
|
||||
group = defaultUser;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == defaultUser) {
|
||||
${defaultUser} = { };
|
||||
};
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.port ];
|
||||
};
|
||||
|
||||
systemd.services.node-red = {
|
||||
description = "Node-RED Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
environment = {
|
||||
HOME = cfg.userDir;
|
||||
};
|
||||
serviceConfig = mkMerge [
|
||||
{
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${finalPackage}/bin/node-red ${pkgs.lib.optionalString cfg.safe "--safe"} --settings ${cfg.configFile} --port ${toString cfg.port} --userDir ${cfg.userDir} ${concatStringsSep " " (mapAttrsToList (name: value: "-D ${name}=${value}") cfg.define)}";
|
||||
PrivateTmp = true;
|
||||
Restart = "always";
|
||||
WorkingDirectory = cfg.userDir;
|
||||
}
|
||||
(mkIf (cfg.userDir == "/var/lib/node-red") { StateDirectory = "node-red"; })
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
34
nixos/modules/services/web-apps/openwebrx.nix
Normal file
34
nixos/modules/services/web-apps/openwebrx.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.services.openwebrx;
|
||||
in
|
||||
{
|
||||
options.services.openwebrx = with lib; {
|
||||
enable = mkEnableOption "OpenWebRX Web interface for Software-Defined Radios on http://localhost:8073";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.openwebrx;
|
||||
defaultText = literalExpression "pkgs.openwebrx";
|
||||
description = "OpenWebRX package to use for the service";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.openwebrx = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = with pkgs; [
|
||||
csdr
|
||||
alsaUtils
|
||||
netcat
|
||||
];
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/openwebrx";
|
||||
Restart = "always";
|
||||
DynamicUser = true;
|
||||
# openwebrx uses /var/lib/openwebrx by default
|
||||
StateDirectory = [ "openwebrx" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
479
nixos/modules/services/web-apps/peertube.nix
Normal file
479
nixos/modules/services/web-apps/peertube.nix
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
{ lib, pkgs, config, options, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.peertube;
|
||||
opt = options.services.peertube;
|
||||
|
||||
settingsFormat = pkgs.formats.json {};
|
||||
configFile = settingsFormat.generate "production.json" cfg.settings;
|
||||
|
||||
env = {
|
||||
NODE_CONFIG_DIR = "/var/lib/peertube/config";
|
||||
NODE_ENV = "production";
|
||||
NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt";
|
||||
NPM_CONFIG_PREFIX = cfg.package;
|
||||
HOME = cfg.package;
|
||||
};
|
||||
|
||||
systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@memlock" "@mount" "@obsolete" "@privileged" "@setuid" ];
|
||||
|
||||
cfgService = {
|
||||
# Proc filesystem
|
||||
ProcSubset = "pid";
|
||||
ProtectProc = "invisible";
|
||||
# Access write directories
|
||||
UMask = "0027";
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "";
|
||||
# Security
|
||||
NoNewPrivileges = true;
|
||||
# Sandboxing
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RemoveIPC = true;
|
||||
PrivateMounts = true;
|
||||
# System Call Filtering
|
||||
SystemCallArchitectures = "native";
|
||||
};
|
||||
|
||||
envFile = pkgs.writeText "peertube.env" (lib.concatMapStrings (s: s + "\n") (
|
||||
(lib.concatLists (lib.mapAttrsToList (name: value:
|
||||
if value != null then [
|
||||
"${name}=\"${toString value}\""
|
||||
] else []
|
||||
) env))));
|
||||
|
||||
peertubeEnv = pkgs.writeShellScriptBin "peertube-env" ''
|
||||
set -a
|
||||
source "${envFile}"
|
||||
eval -- "\$@"
|
||||
'';
|
||||
|
||||
peertubeCli = pkgs.writeShellScriptBin "peertube" ''
|
||||
node ~/dist/server/tools/peertube.js $@
|
||||
'';
|
||||
|
||||
in {
|
||||
options.services.peertube = {
|
||||
enable = lib.mkEnableOption "Enable Peertube’s service";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "peertube";
|
||||
description = "User account under which Peertube runs.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "peertube";
|
||||
description = "Group under which Peertube runs.";
|
||||
};
|
||||
|
||||
localDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "peertube.example.com";
|
||||
description = "The domain serving your PeerTube instance.";
|
||||
};
|
||||
|
||||
listenHttp = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 9000;
|
||||
description = "listen port for HTTP server.";
|
||||
};
|
||||
|
||||
listenWeb = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 9000;
|
||||
description = "listen port for WEB server.";
|
||||
};
|
||||
|
||||
enableWebHttps = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Enable or disable HTTPS protocol.";
|
||||
};
|
||||
|
||||
dataDirs = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [ ];
|
||||
example = [ "/opt/peertube/storage" "/var/cache/peertube" ];
|
||||
description = "Allow access to custom data locations.";
|
||||
};
|
||||
|
||||
serviceEnvironmentFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/peertube/password-init-root";
|
||||
description = ''
|
||||
Set environment variables for the service. Mainly useful for setting the initial root password.
|
||||
For example write to file:
|
||||
PT_INITIAL_ROOT_PASSWORD=changeme
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = settingsFormat.type;
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
listen = {
|
||||
hostname = "0.0.0.0";
|
||||
};
|
||||
log = {
|
||||
level = "debug";
|
||||
};
|
||||
storage = {
|
||||
tmp = "/opt/data/peertube/storage/tmp/";
|
||||
logs = "/opt/data/peertube/storage/logs/";
|
||||
cache = "/opt/data/peertube/storage/cache/";
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = "Configuration for peertube.";
|
||||
};
|
||||
|
||||
database = {
|
||||
createLocally = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Configure local PostgreSQL database server for PeerTube.";
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if cfg.database.createLocally then "/run/postgresql" else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.database.createLocally}
|
||||
then "/run/postgresql"
|
||||
else null
|
||||
'';
|
||||
example = "192.168.15.47";
|
||||
description = "Database host address or unix socket.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 5432;
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "peertube";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "peertube";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/peertube/password-posgressql-db";
|
||||
description = "Password for PostgreSQL database.";
|
||||
};
|
||||
};
|
||||
|
||||
redis = {
|
||||
createLocally = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Configure local Redis server for PeerTube.";
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket}
|
||||
then "127.0.0.1"
|
||||
else null
|
||||
'';
|
||||
description = "Redis host.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.port;
|
||||
default = if cfg.redis.createLocally && cfg.redis.enableUnixSocket then null else 31638;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.${opt.redis.createLocally} && config.${opt.redis.enableUnixSocket}
|
||||
then null
|
||||
else 6379
|
||||
'';
|
||||
description = "Redis port.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/peertube/password-redis-db";
|
||||
description = "Password for redis database.";
|
||||
};
|
||||
|
||||
enableUnixSocket = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.redis.createLocally;
|
||||
defaultText = lib.literalExpression "config.${opt.redis.createLocally}";
|
||||
description = "Use Unix socket.";
|
||||
};
|
||||
};
|
||||
|
||||
smtp = {
|
||||
createLocally = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Configure local Postfix SMTP server for PeerTube.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/run/keys/peertube/password-smtp";
|
||||
description = "Password for smtp server.";
|
||||
};
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.peertube;
|
||||
defaultText = lib.literalExpression "pkgs.peertube";
|
||||
description = "Peertube package to use.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = cfg.serviceEnvironmentFile == null || !lib.hasPrefix builtins.storeDir cfg.serviceEnvironmentFile;
|
||||
message = ''
|
||||
<option>services.peertube.serviceEnvironmentFile</option> points to
|
||||
a file in the Nix store. You should use a quoted absolute path to
|
||||
prevent this.
|
||||
'';
|
||||
}
|
||||
{ assertion = !(cfg.redis.enableUnixSocket && (cfg.redis.host != null || cfg.redis.port != null));
|
||||
message = ''
|
||||
<option>services.peertube.redis.createLocally</option> and redis network connection (<option>services.peertube.redis.host</option> or <option>services.peertube.redis.port</option>) enabled. Disable either of them.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.redis.enableUnixSocket || (cfg.redis.host != null && cfg.redis.port != null);
|
||||
message = ''
|
||||
<option>services.peertube.redis.host</option> and <option>services.peertube.redis.port</option> needs to be set if <option>services.peertube.redis.enableUnixSocket</option> is not enabled.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.redis.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.redis.passwordFile;
|
||||
message = ''
|
||||
<option>services.peertube.redis.passwordFile</option> points to
|
||||
a file in the Nix store. You should use a quoted absolute path to
|
||||
prevent this.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.database.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.database.passwordFile;
|
||||
message = ''
|
||||
<option>services.peertube.database.passwordFile</option> points to
|
||||
a file in the Nix store. You should use a quoted absolute path to
|
||||
prevent this.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.smtp.passwordFile == null || !lib.hasPrefix builtins.storeDir cfg.smtp.passwordFile;
|
||||
message = ''
|
||||
<option>services.peertube.smtp.passwordFile</option> points to
|
||||
a file in the Nix store. You should use a quoted absolute path to
|
||||
prevent this.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
services.peertube.settings = lib.mkMerge [
|
||||
{
|
||||
listen = {
|
||||
port = cfg.listenHttp;
|
||||
};
|
||||
webserver = {
|
||||
https = (if cfg.enableWebHttps then true else false);
|
||||
hostname = "${cfg.localDomain}";
|
||||
port = cfg.listenWeb;
|
||||
};
|
||||
database = {
|
||||
hostname = "${cfg.database.host}";
|
||||
port = cfg.database.port;
|
||||
name = "${cfg.database.name}";
|
||||
username = "${cfg.database.user}";
|
||||
};
|
||||
redis = {
|
||||
hostname = "${toString cfg.redis.host}";
|
||||
port = (if cfg.redis.port == null then "" else cfg.redis.port);
|
||||
};
|
||||
storage = {
|
||||
tmp = lib.mkDefault "/var/lib/peertube/storage/tmp/";
|
||||
bin = lib.mkDefault "/var/lib/peertube/storage/bin/";
|
||||
avatars = lib.mkDefault "/var/lib/peertube/storage/avatars/";
|
||||
videos = lib.mkDefault "/var/lib/peertube/storage/videos/";
|
||||
streaming_playlists = lib.mkDefault "/var/lib/peertube/storage/streaming-playlists/";
|
||||
redundancy = lib.mkDefault "/var/lib/peertube/storage/redundancy/";
|
||||
logs = lib.mkDefault "/var/lib/peertube/storage/logs/";
|
||||
previews = lib.mkDefault "/var/lib/peertube/storage/previews/";
|
||||
thumbnails = lib.mkDefault "/var/lib/peertube/storage/thumbnails/";
|
||||
torrents = lib.mkDefault "/var/lib/peertube/storage/torrents/";
|
||||
captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
|
||||
cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
|
||||
plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
|
||||
client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
|
||||
};
|
||||
import = {
|
||||
videos = {
|
||||
http = {
|
||||
youtube_dl_release = {
|
||||
python_path = "${pkgs.python3}/bin/python";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
(lib.mkIf cfg.redis.enableUnixSocket { redis = { socket = "/run/redis-peertube/redis.sock"; }; })
|
||||
];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
|
||||
"z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally {
|
||||
description = "Initialization database for PeerTube daemon";
|
||||
after = [ "network.target" "postgresql.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
script = let
|
||||
psqlSetupCommands = pkgs.writeText "peertube-init.sql" ''
|
||||
SELECT 'CREATE USER "${cfg.database.user}"' WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${cfg.database.user}')\gexec
|
||||
SELECT 'CREATE DATABASE "${cfg.database.name}" OWNER "${cfg.database.user}" TEMPLATE template0 ENCODING UTF8' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${cfg.database.name}')\gexec
|
||||
\c '${cfg.database.name}'
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
'';
|
||||
in "${config.services.postgresql.package}/bin/psql -f ${psqlSetupCommands}";
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
WorkingDirectory = cfg.package;
|
||||
# User and group
|
||||
User = "postgres";
|
||||
Group = "postgres";
|
||||
# Sandboxing
|
||||
RestrictAddressFamilies = [ "AF_UNIX" ];
|
||||
MemoryDenyWriteExecute = true;
|
||||
# System Call Filtering
|
||||
SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]);
|
||||
} // cfgService;
|
||||
};
|
||||
|
||||
systemd.services.peertube = {
|
||||
description = "PeerTube daemon";
|
||||
after = [ "network.target" ]
|
||||
++ lib.optionals cfg.redis.createLocally [ "redis.service" ]
|
||||
++ lib.optionals cfg.database.createLocally [ "postgresql.service" "peertube-init-db.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = env;
|
||||
|
||||
path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn python3 ];
|
||||
|
||||
script = ''
|
||||
#!/bin/sh
|
||||
umask 077
|
||||
cat > /var/lib/peertube/config/local.yaml <<EOF
|
||||
${lib.optionalString ((!cfg.database.createLocally) && (cfg.database.passwordFile != null)) ''
|
||||
database:
|
||||
password: '$(cat ${cfg.database.passwordFile})'
|
||||
''}
|
||||
${lib.optionalString (cfg.redis.passwordFile != null) ''
|
||||
redis:
|
||||
auth: '$(cat ${cfg.redis.passwordFile})'
|
||||
''}
|
||||
${lib.optionalString (cfg.smtp.passwordFile != null) ''
|
||||
smtp:
|
||||
password: '$(cat ${cfg.smtp.passwordFile})'
|
||||
''}
|
||||
EOF
|
||||
ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml
|
||||
ln -sf ${configFile} /var/lib/peertube/config/production.json
|
||||
npm start
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
RestartSec = 20;
|
||||
TimeoutSec = 60;
|
||||
WorkingDirectory = cfg.package;
|
||||
# User and group
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
# State directory and mode
|
||||
StateDirectory = "peertube";
|
||||
StateDirectoryMode = "0750";
|
||||
# Access write directories
|
||||
ReadWritePaths = cfg.dataDirs;
|
||||
# Environment
|
||||
EnvironmentFile = cfg.serviceEnvironmentFile;
|
||||
# Sandboxing
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
|
||||
MemoryDenyWriteExecute = false;
|
||||
# System Call Filtering
|
||||
SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "pipe" "pipe2" ];
|
||||
} // cfgService;
|
||||
};
|
||||
|
||||
services.postgresql = lib.mkIf cfg.database.createLocally {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.redis.servers.peertube = lib.mkMerge [
|
||||
(lib.mkIf cfg.redis.createLocally {
|
||||
enable = true;
|
||||
})
|
||||
(lib.mkIf (cfg.redis.createLocally && !cfg.redis.enableUnixSocket) {
|
||||
bind = "127.0.0.1";
|
||||
port = cfg.redis.port;
|
||||
})
|
||||
(lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
|
||||
unixSocket = "/run/redis-peertube/redis.sock";
|
||||
unixSocketPerm = 660;
|
||||
})
|
||||
];
|
||||
|
||||
services.postfix = lib.mkIf cfg.smtp.createLocally {
|
||||
enable = true;
|
||||
hostname = lib.mkDefault "${cfg.localDomain}";
|
||||
};
|
||||
|
||||
users.users = lib.mkMerge [
|
||||
(lib.mkIf (cfg.user == "peertube") {
|
||||
peertube = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.package;
|
||||
};
|
||||
})
|
||||
(lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
|
||||
(lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
|
||||
];
|
||||
|
||||
users.groups = lib.optionalAttrs (cfg.group == "peertube") {
|
||||
peertube = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
78
nixos/modules/services/web-apps/pgpkeyserver-lite.nix
Normal file
78
nixos/modules/services/web-apps/pgpkeyserver-lite.nix
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{ config, lib, options, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.pgpkeyserver-lite;
|
||||
sksCfg = config.services.sks;
|
||||
sksOpt = options.services.sks;
|
||||
|
||||
webPkg = cfg.package;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options = {
|
||||
|
||||
services.pgpkeyserver-lite = {
|
||||
|
||||
enable = mkEnableOption "pgpkeyserver-lite on a nginx vHost proxying to a gpg keyserver";
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.pgpkeyserver-lite;
|
||||
defaultText = literalExpression "pkgs.pgpkeyserver-lite";
|
||||
type = types.package;
|
||||
description = "
|
||||
Which webgui derivation to use.
|
||||
";
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
description = "
|
||||
Which hostname to set the vHost to that is proxying to sks.
|
||||
";
|
||||
};
|
||||
|
||||
hkpAddress = mkOption {
|
||||
default = builtins.head sksCfg.hkpAddress;
|
||||
defaultText = literalExpression "head config.${sksOpt.hkpAddress}";
|
||||
type = types.str;
|
||||
description = "
|
||||
Wich ip address the sks-keyserver is listening on.
|
||||
";
|
||||
};
|
||||
|
||||
hkpPort = mkOption {
|
||||
default = sksCfg.hkpPort;
|
||||
defaultText = literalExpression "config.${sksOpt.hkpPort}";
|
||||
type = types.int;
|
||||
description = "
|
||||
Which port the sks-keyserver is listening on.
|
||||
";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.nginx.enable = true;
|
||||
|
||||
services.nginx.virtualHosts = let
|
||||
hkpPort = builtins.toString cfg.hkpPort;
|
||||
in {
|
||||
${cfg.hostname} = {
|
||||
root = webPkg;
|
||||
locations = {
|
||||
"/pks".extraConfig = ''
|
||||
proxy_pass http://${cfg.hkpAddress}:${hkpPort};
|
||||
proxy_pass_header Server;
|
||||
add_header Via "1.1 ${cfg.hostname}";
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
88
nixos/modules/services/web-apps/pict-rs.md
Normal file
88
nixos/modules/services/web-apps/pict-rs.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Pict-rs {#module-services-pict-rs}
|
||||
|
||||
pict-rs is a a simple image hosting service.
|
||||
|
||||
## Quickstart {#module-services-pict-rs-quickstart}
|
||||
|
||||
the minimum to start pict-rs is
|
||||
|
||||
```nix
|
||||
services.pict-rs.enable = true;
|
||||
```
|
||||
|
||||
this will start the http server on port 8080 by default.
|
||||
|
||||
## Usage {#module-services-pict-rs-usage}
|
||||
|
||||
pict-rs offers the following endpoints:
|
||||
- `POST /image` for uploading an image. Uploaded content must be valid multipart/form-data with an
|
||||
image array located within the `images[]` key
|
||||
|
||||
This endpoint returns the following JSON structure on success with a 201 Created status
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"delete_token": "JFvFhqJA98",
|
||||
"file": "lkWZDRvugm.jpg"
|
||||
},
|
||||
{
|
||||
"delete_token": "kAYy9nk2WK",
|
||||
"file": "8qFS0QooAn.jpg"
|
||||
},
|
||||
{
|
||||
"delete_token": "OxRpM3sf0Y",
|
||||
"file": "1hJaYfGE01.jpg"
|
||||
}
|
||||
],
|
||||
"msg": "ok"
|
||||
}
|
||||
```
|
||||
- `GET /image/download?url=...` Download an image from a remote server, returning the same JSON
|
||||
payload as the `POST` endpoint
|
||||
- `GET /image/original/{file}` for getting a full-resolution image. `file` here is the `file` key from the
|
||||
`/image` endpoint's JSON
|
||||
- `GET /image/details/original/{file}` for getting the details of a full-resolution image.
|
||||
The returned JSON is structured like so:
|
||||
```json
|
||||
{
|
||||
"width": 800,
|
||||
"height": 537,
|
||||
"content_type": "image/webp",
|
||||
"created_at": [
|
||||
2020,
|
||||
345,
|
||||
67376,
|
||||
394363487
|
||||
]
|
||||
}
|
||||
```
|
||||
- `GET /image/process.{ext}?src={file}&...` get a file with transformations applied.
|
||||
existing transformations include
|
||||
- `identity=true`: apply no changes
|
||||
- `blur={float}`: apply a gaussian blur to the file
|
||||
- `thumbnail={int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}`
|
||||
square using raw pixel sampling
|
||||
- `resize={int}`: produce a thumbnail of the image fitting inside an `{int}` by `{int}` square
|
||||
using a Lanczos2 filter. This is slower than sampling but looks a bit better in some cases
|
||||
- `crop={int-w}x{int-h}`: produce a cropped version of the image with an `{int-w}` by `{int-h}`
|
||||
aspect ratio. The resulting crop will be centered on the image. Either the width or height
|
||||
of the image will remain full-size, depending on the image's aspect ratio and the requested
|
||||
aspect ratio. For example, a 1600x900 image cropped with a 1x1 aspect ratio will become 900x900. A
|
||||
1600x1100 image cropped with a 16x9 aspect ratio will become 1600x900.
|
||||
|
||||
Supported `ext` file extensions include `png`, `jpg`, and `webp`
|
||||
|
||||
An example of usage could be
|
||||
```
|
||||
GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0
|
||||
```
|
||||
which would create a 256x256px JPEG thumbnail and blur it
|
||||
- `GET /image/details/process.{ext}?src={file}&...` for getting the details of a processed image.
|
||||
The returned JSON is the same format as listed for the full-resolution details endpoint.
|
||||
- `DELETE /image/delete/{delete_token}/{file}` or `GET /image/delete/{delete_token}/{file}` to
|
||||
delete a file, where `delete_token` and `file` are from the `/image` endpoint's JSON
|
||||
|
||||
## Missing {#module-services-pict-rs-missing}
|
||||
|
||||
- Configuring the secure-api-key is not included yet. The envisioned basic use case is consumption on localhost by other services without exposing the service to the internet.
|
||||
50
nixos/modules/services/web-apps/pict-rs.nix
Normal file
50
nixos/modules/services/web-apps/pict-rs.nix
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.pict-rs;
|
||||
in
|
||||
{
|
||||
meta.maintainers = with maintainers; [ happysalada ];
|
||||
# Don't edit the docbook xml directly, edit the md and generate it:
|
||||
# `pandoc pict-rs.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > pict-rs.xml`
|
||||
meta.doc = ./pict-rs.xml;
|
||||
|
||||
options.services.pict-rs = {
|
||||
enable = mkEnableOption "pict-rs server";
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/pict-rs";
|
||||
description = ''
|
||||
The directory where to store the uploaded images.
|
||||
'';
|
||||
};
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = ''
|
||||
The IPv4 address to deploy the service to.
|
||||
'';
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8080;
|
||||
description = ''
|
||||
The port which to bind the service to.
|
||||
'';
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.pict-rs = {
|
||||
environment = {
|
||||
PICTRS_PATH = cfg.dataDir;
|
||||
PICTRS_ADDR = "${cfg.address}:${toString cfg.port}";
|
||||
};
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
StateDirectory = "pict-rs";
|
||||
ExecStart = "${pkgs.pict-rs}/bin/pict-rs";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
162
nixos/modules/services/web-apps/pict-rs.xml
Normal file
162
nixos/modules/services/web-apps/pict-rs.xml
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-pict-rs">
|
||||
<title>Pict-rs</title>
|
||||
<para>
|
||||
pict-rs is a a simple image hosting service.
|
||||
</para>
|
||||
<section xml:id="module-services-pict-rs-quickstart">
|
||||
<title>Quickstart</title>
|
||||
<para>
|
||||
the minimum to start pict-rs is
|
||||
</para>
|
||||
<programlisting language="bash">
|
||||
services.pict-rs.enable = true;
|
||||
</programlisting>
|
||||
<para>
|
||||
this will start the http server on port 8080 by default.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-pict-rs-usage">
|
||||
<title>Usage</title>
|
||||
<para>
|
||||
pict-rs offers the following endpoints: -
|
||||
<literal>POST /image</literal> for uploading an image. Uploaded
|
||||
content must be valid multipart/form-data with an image array
|
||||
located within the <literal>images[]</literal> key
|
||||
</para>
|
||||
<programlisting>
|
||||
This endpoint returns the following JSON structure on success with a 201 Created status
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"delete_token": "JFvFhqJA98",
|
||||
"file": "lkWZDRvugm.jpg"
|
||||
},
|
||||
{
|
||||
"delete_token": "kAYy9nk2WK",
|
||||
"file": "8qFS0QooAn.jpg"
|
||||
},
|
||||
{
|
||||
"delete_token": "OxRpM3sf0Y",
|
||||
"file": "1hJaYfGE01.jpg"
|
||||
}
|
||||
],
|
||||
"msg": "ok"
|
||||
}
|
||||
```
|
||||
</programlisting>
|
||||
<itemizedlist>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>GET /image/download?url=...</literal> Download an
|
||||
image from a remote server, returning the same JSON payload as
|
||||
the <literal>POST</literal> endpoint
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>GET /image/original/{file}</literal> for getting a
|
||||
full-resolution image. <literal>file</literal> here is the
|
||||
<literal>file</literal> key from the <literal>/image</literal>
|
||||
endpoint’s JSON
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>GET /image/details/original/{file}</literal> for
|
||||
getting the details of a full-resolution image. The returned
|
||||
JSON is structured like so:
|
||||
<literal>json { "width": 800, "height": 537, "content_type": "image/webp", "created_at": [ 2020, 345, 67376, 394363487 ] }</literal>
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>GET /image/process.{ext}?src={file}&...</literal>
|
||||
get a file with transformations applied. existing
|
||||
transformations include
|
||||
</para>
|
||||
<itemizedlist spacing="compact">
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>identity=true</literal>: apply no changes
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>blur={float}</literal>: apply a gaussian blur to
|
||||
the file
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>thumbnail={int}</literal>: produce a thumbnail of
|
||||
the image fitting inside an <literal>{int}</literal> by
|
||||
<literal>{int}</literal> square using raw pixel sampling
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>resize={int}</literal>: produce a thumbnail of
|
||||
the image fitting inside an <literal>{int}</literal> by
|
||||
<literal>{int}</literal> square using a Lanczos2 filter.
|
||||
This is slower than sampling but looks a bit better in
|
||||
some cases
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>crop={int-w}x{int-h}</literal>: produce a cropped
|
||||
version of the image with an <literal>{int-w}</literal> by
|
||||
<literal>{int-h}</literal> aspect ratio. The resulting
|
||||
crop will be centered on the image. Either the width or
|
||||
height of the image will remain full-size, depending on
|
||||
the image’s aspect ratio and the requested aspect ratio.
|
||||
For example, a 1600x900 image cropped with a 1x1 aspect
|
||||
ratio will become 900x900. A 1600x1100 image cropped with
|
||||
a 16x9 aspect ratio will become 1600x900.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
<para>
|
||||
Supported <literal>ext</literal> file extensions include
|
||||
<literal>png</literal>, <literal>jpg</literal>, and
|
||||
<literal>webp</literal>
|
||||
</para>
|
||||
<para>
|
||||
An example of usage could be
|
||||
<literal>GET /image/process.jpg?src=asdf.png&thumbnail=256&blur=3.0</literal>
|
||||
which would create a 256x256px JPEG thumbnail and blur it
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>GET /image/details/process.{ext}?src={file}&...</literal>
|
||||
for getting the details of a processed image. The returned
|
||||
JSON is the same format as listed for the full-resolution
|
||||
details endpoint.
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
<literal>DELETE /image/delete/{delete_token}/{file}</literal>
|
||||
or <literal>GET /image/delete/{delete_token}/{file}</literal>
|
||||
to delete a file, where <literal>delete_token</literal> and
|
||||
<literal>file</literal> are from the <literal>/image</literal>
|
||||
endpoint’s JSON
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
<section xml:id="module-services-pict-rs-missing">
|
||||
<title>Missing</title>
|
||||
<itemizedlist spacing="compact">
|
||||
<listitem>
|
||||
<para>
|
||||
Configuring the secure-api-key is not included yet. The
|
||||
envisioned basic use case is consumption on localhost by other
|
||||
services without exposing the service to the internet.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</section>
|
||||
</chapter>
|
||||
140
nixos/modules/services/web-apps/plantuml-server.nix
Normal file
140
nixos/modules/services/web-apps/plantuml-server.nix
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.plantuml-server;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
services.plantuml-server = {
|
||||
enable = mkEnableOption "PlantUML server";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.plantuml-server;
|
||||
defaultText = literalExpression "pkgs.plantuml-server";
|
||||
description = "PlantUML server package to use";
|
||||
};
|
||||
|
||||
packages = {
|
||||
jdk = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jdk;
|
||||
defaultText = literalExpression "pkgs.jdk";
|
||||
description = "JDK package to use for the server";
|
||||
};
|
||||
jetty = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.jetty;
|
||||
defaultText = literalExpression "pkgs.jetty";
|
||||
description = "Jetty package to use for the server";
|
||||
};
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "plantuml";
|
||||
description = "User which runs PlantUML server.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "plantuml";
|
||||
description = "Group which runs PlantUML server.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/plantuml";
|
||||
description = "Home directory of the PlantUML server instance.";
|
||||
};
|
||||
|
||||
listenHost = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Host to listen on.";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 8080;
|
||||
description = "Port to listen on.";
|
||||
};
|
||||
|
||||
plantumlLimitSize = mkOption {
|
||||
type = types.int;
|
||||
default = 4096;
|
||||
description = "Limits image width and height.";
|
||||
};
|
||||
|
||||
graphvizPackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.graphviz;
|
||||
defaultText = literalExpression "pkgs.graphviz";
|
||||
description = "Package containing the dot executable.";
|
||||
};
|
||||
|
||||
plantumlStats = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Set it to on to enable statistics report (https://plantuml.com/statistics-report).";
|
||||
};
|
||||
|
||||
httpAuthorization = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "When calling the proxy endpoint, the value of HTTP_AUTHORIZATION will be used to set the HTTP Authorization header.";
|
||||
};
|
||||
|
||||
allowPlantumlInclude = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enables !include processing which can read files from the server into diagrams. Files are read relative to the current working directory.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = cfg.home;
|
||||
createHome = true;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {};
|
||||
|
||||
systemd.services.plantuml-server = {
|
||||
description = "PlantUML server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ cfg.home ];
|
||||
environment = {
|
||||
PLANTUML_LIMIT_SIZE = builtins.toString cfg.plantumlLimitSize;
|
||||
GRAPHVIZ_DOT = "${cfg.graphvizPackage}/bin/dot";
|
||||
PLANTUML_STATS = if cfg.plantumlStats then "on" else "off";
|
||||
HTTP_AUTHORIZATION = cfg.httpAuthorization;
|
||||
ALLOW_PLANTUML_INCLUDE = if cfg.allowPlantumlInclude then "true" else "false";
|
||||
};
|
||||
script = ''
|
||||
${cfg.packages.jdk}/bin/java \
|
||||
-jar ${cfg.packages.jetty}/start.jar \
|
||||
--module=deploy,http,jsp \
|
||||
jetty.home=${cfg.packages.jetty} \
|
||||
jetty.base=${cfg.package} \
|
||||
jetty.http.host=${cfg.listenHost} \
|
||||
jetty.http.port=${builtins.toString cfg.listenPort}
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ truh ];
|
||||
}
|
||||
292
nixos/modules/services/web-apps/plausible.nix
Normal file
292
nixos/modules/services/web-apps/plausible.nix
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.plausible;
|
||||
|
||||
in {
|
||||
options.services.plausible = {
|
||||
enable = mkEnableOption "plausible";
|
||||
|
||||
releaseCookiePath = mkOption {
|
||||
type = with types; either str path;
|
||||
description = ''
|
||||
The path to the file with release cookie. (used for remote connection to the running node).
|
||||
'';
|
||||
};
|
||||
|
||||
adminUser = {
|
||||
name = mkOption {
|
||||
default = "admin";
|
||||
type = types.str;
|
||||
description = ''
|
||||
Name of the admin user that plausible will created on initial startup.
|
||||
'';
|
||||
};
|
||||
|
||||
email = mkOption {
|
||||
type = types.str;
|
||||
example = "admin@localhost";
|
||||
description = ''
|
||||
Email-address of the admin-user.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.either types.str types.path;
|
||||
description = ''
|
||||
Path to the file which contains the password of the admin user.
|
||||
'';
|
||||
};
|
||||
|
||||
activate = mkEnableOption "activating the freshly created admin-user";
|
||||
};
|
||||
|
||||
database = {
|
||||
clickhouse = {
|
||||
setup = mkEnableOption "creating a clickhouse instance" // { default = true; };
|
||||
url = mkOption {
|
||||
default = "http://localhost:8123/default";
|
||||
type = types.str;
|
||||
description = ''
|
||||
The URL to be used to connect to <package>clickhouse</package>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
postgres = {
|
||||
setup = mkEnableOption "creating a postgresql instance" // { default = true; };
|
||||
dbname = mkOption {
|
||||
default = "plausible";
|
||||
type = types.str;
|
||||
description = ''
|
||||
Name of the database to use.
|
||||
'';
|
||||
};
|
||||
socket = mkOption {
|
||||
default = "/run/postgresql";
|
||||
type = types.str;
|
||||
description = ''
|
||||
Path to the UNIX domain-socket to communicate with <package>postgres</package>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
server = {
|
||||
disableRegistration = mkOption {
|
||||
default = true;
|
||||
type = types.bool;
|
||||
description = ''
|
||||
Whether to prohibit creating an account in plausible's UI.
|
||||
'';
|
||||
};
|
||||
secretKeybaseFile = mkOption {
|
||||
type = types.either types.path types.str;
|
||||
description = ''
|
||||
Path to the secret used by the <literal>phoenix</literal>-framework. Instructions
|
||||
how to generate one are documented in the
|
||||
<link xlink:href="https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Secret.html#content">
|
||||
framework docs</link>.
|
||||
'';
|
||||
};
|
||||
port = mkOption {
|
||||
default = 8000;
|
||||
type = types.port;
|
||||
description = ''
|
||||
Port where the service should be available.
|
||||
'';
|
||||
};
|
||||
baseUrl = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Public URL where plausible is available.
|
||||
|
||||
Note that <literal>/path</literal> components are currently ignored:
|
||||
<link xlink:href="https://github.com/plausible/analytics/issues/1182">
|
||||
https://github.com/plausible/analytics/issues/1182
|
||||
</link>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
mail = {
|
||||
email = mkOption {
|
||||
default = "hello@plausible.local";
|
||||
type = types.str;
|
||||
description = ''
|
||||
The email id to use for as <emphasis>from</emphasis> address of all communications
|
||||
from Plausible.
|
||||
'';
|
||||
};
|
||||
smtp = {
|
||||
hostAddr = mkOption {
|
||||
default = "localhost";
|
||||
type = types.str;
|
||||
description = ''
|
||||
The host address of your smtp server.
|
||||
'';
|
||||
};
|
||||
hostPort = mkOption {
|
||||
default = 25;
|
||||
type = types.port;
|
||||
description = ''
|
||||
The port of your smtp server.
|
||||
'';
|
||||
};
|
||||
user = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.str;
|
||||
description = ''
|
||||
The username/email in case SMTP auth is enabled.
|
||||
'';
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
default = null;
|
||||
type = with types; nullOr (either str path);
|
||||
description = ''
|
||||
The path to the file with the password in case SMTP auth is enabled.
|
||||
'';
|
||||
};
|
||||
enableSSL = mkEnableOption "SSL when connecting to the SMTP server";
|
||||
retries = mkOption {
|
||||
type = types.ints.unsigned;
|
||||
default = 2;
|
||||
description = ''
|
||||
Number of retries to make until mailer gives up.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = cfg.adminUser.activate -> cfg.database.postgres.setup;
|
||||
message = ''
|
||||
Unable to automatically activate the admin-user if no locally managed DB for
|
||||
postgres (`services.plausible.database.postgres.setup') is enabled!
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
services.postgresql = mkIf cfg.database.postgres.setup {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.clickhouse = mkIf cfg.database.clickhouse.setup {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.epmd.enable = true;
|
||||
|
||||
environment.systemPackages = [ pkgs.plausible ];
|
||||
|
||||
systemd.services = mkMerge [
|
||||
{
|
||||
plausible = {
|
||||
inherit (pkgs.plausible.meta) description;
|
||||
documentation = [ "https://plausible.io/docs/self-hosting" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = optionals cfg.database.postgres.setup [ "postgresql.service" "plausible-postgres.service" ];
|
||||
requires = optional cfg.database.clickhouse.setup "clickhouse.service"
|
||||
++ optionals cfg.database.postgres.setup [
|
||||
"postgresql.service"
|
||||
"plausible-postgres.service"
|
||||
];
|
||||
|
||||
environment = {
|
||||
# NixOS specific option to avoid that it's trying to write into its store-path.
|
||||
# See also https://github.com/lau/tzdata#data-directory-and-releases
|
||||
STORAGE_DIR = "/var/lib/plausible/elixir_tzdata";
|
||||
|
||||
# Configuration options from
|
||||
# https://plausible.io/docs/self-hosting-configuration
|
||||
PORT = toString cfg.server.port;
|
||||
DISABLE_REGISTRATION = boolToString cfg.server.disableRegistration;
|
||||
|
||||
RELEASE_TMP = "/var/lib/plausible/tmp";
|
||||
# Home is needed to connect to the node with iex
|
||||
HOME = "/var/lib/plausible";
|
||||
|
||||
ADMIN_USER_NAME = cfg.adminUser.name;
|
||||
ADMIN_USER_EMAIL = cfg.adminUser.email;
|
||||
|
||||
DATABASE_SOCKET_DIR = cfg.database.postgres.socket;
|
||||
DATABASE_NAME = cfg.database.postgres.dbname;
|
||||
CLICKHOUSE_DATABASE_URL = cfg.database.clickhouse.url;
|
||||
|
||||
BASE_URL = cfg.server.baseUrl;
|
||||
|
||||
MAILER_EMAIL = cfg.mail.email;
|
||||
SMTP_HOST_ADDR = cfg.mail.smtp.hostAddr;
|
||||
SMTP_HOST_PORT = toString cfg.mail.smtp.hostPort;
|
||||
SMTP_RETRIES = toString cfg.mail.smtp.retries;
|
||||
SMTP_HOST_SSL_ENABLED = boolToString cfg.mail.smtp.enableSSL;
|
||||
|
||||
SELFHOST = "true";
|
||||
} // (optionalAttrs (cfg.mail.smtp.user != null) {
|
||||
SMTP_USER_NAME = cfg.mail.smtp.user;
|
||||
});
|
||||
|
||||
path = [ pkgs.plausible ]
|
||||
++ optional cfg.database.postgres.setup config.services.postgresql.package;
|
||||
script = ''
|
||||
export CONFIG_DIR=$CREDENTIALS_DIRECTORY
|
||||
|
||||
export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
|
||||
|
||||
# setup
|
||||
${pkgs.plausible}/createdb.sh
|
||||
${pkgs.plausible}/migrate.sh
|
||||
${optionalString cfg.adminUser.activate ''
|
||||
if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
|
||||
psql -d plausible <<< "UPDATE users SET email_verified=true;"
|
||||
fi
|
||||
''}
|
||||
|
||||
exec plausible start
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
PrivateTmp = true;
|
||||
WorkingDirectory = "/var/lib/plausible";
|
||||
StateDirectory = "plausible";
|
||||
LoadCredential = [
|
||||
"ADMIN_USER_PWD:${cfg.adminUser.passwordFile}"
|
||||
"SECRET_KEY_BASE:${cfg.server.secretKeybaseFile}"
|
||||
"RELEASE_COOKIE:${cfg.releaseCookiePath}"
|
||||
] ++ lib.optionals (cfg.mail.smtp.passwordFile != null) [ "SMTP_USER_PWD:${cfg.mail.smtp.passwordFile}"];
|
||||
};
|
||||
};
|
||||
}
|
||||
(mkIf cfg.database.postgres.setup {
|
||||
# `plausible' requires the `citext'-extension.
|
||||
plausible-postgres = {
|
||||
after = [ "postgresql.service" ];
|
||||
partOf = [ "plausible.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = config.services.postgresql.superUser;
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = with cfg.database.postgres; ''
|
||||
PSQL() {
|
||||
${config.services.postgresql.package}/bin/psql --port=5432 "$@"
|
||||
}
|
||||
# check if the database already exists
|
||||
if ! PSQL -lqt | ${pkgs.coreutils}/bin/cut -d \| -f 1 | ${pkgs.gnugrep}/bin/grep -qw ${dbname} ; then
|
||||
PSQL -tAc "CREATE ROLE plausible WITH LOGIN;"
|
||||
PSQL -tAc "CREATE DATABASE ${dbname} WITH OWNER plausible;"
|
||||
PSQL -d ${dbname} -tAc "CREATE EXTENSION IF NOT EXISTS citext;"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ ma27 ];
|
||||
meta.doc = ./plausible.xml;
|
||||
}
|
||||
51
nixos/modules/services/web-apps/plausible.xml
Normal file
51
nixos/modules/services/web-apps/plausible.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-plausible">
|
||||
<title>Plausible</title>
|
||||
<para>
|
||||
<link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
|
||||
Google analytics.
|
||||
</para>
|
||||
<section xml:id="module-services-plausible-basic-usage">
|
||||
<title>Basic Usage</title>
|
||||
<para>
|
||||
At first, a secret key is needed to be generated. This can be done with e.g.
|
||||
<screen><prompt>$ </prompt>openssl rand -base64 64</screen>
|
||||
</para>
|
||||
<para>
|
||||
After that, <package>plausible</package> can be deployed like this:
|
||||
<programlisting>{
|
||||
services.plausible = {
|
||||
<link linkend="opt-services.plausible.enable">enable</link> = true;
|
||||
adminUser = {
|
||||
<link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
|
||||
<link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
|
||||
<link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
|
||||
};
|
||||
server = {
|
||||
<link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
|
||||
<link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
|
||||
};
|
||||
};
|
||||
}</programlisting>
|
||||
<calloutlist>
|
||||
<callout arearefs='ex-plausible-cfg-activate'>
|
||||
<para>
|
||||
<varname>activate</varname> is used to skip the email verification of the admin-user that's
|
||||
automatically created by <package>plausible</package>. This is only supported if
|
||||
<package>postgresql</package> is configured by the module. This is done by default, but
|
||||
can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
|
||||
</para>
|
||||
</callout>
|
||||
<callout arearefs='ex-plausible-cfg-secretbase'>
|
||||
<para>
|
||||
<varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
|
||||
with <package>openssl</package> as described above.
|
||||
</para>
|
||||
</callout>
|
||||
</calloutlist>
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
152
nixos/modules/services/web-apps/powerdns-admin.nix
Normal file
152
nixos/modules/services/web-apps/powerdns-admin.nix
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.powerdns-admin;
|
||||
|
||||
configText = ''
|
||||
${cfg.config}
|
||||
''
|
||||
+ optionalString (cfg.secretKeyFile != null) ''
|
||||
with open('${cfg.secretKeyFile}') as file:
|
||||
SECRET_KEY = file.read()
|
||||
''
|
||||
+ optionalString (cfg.saltFile != null) ''
|
||||
with open('${cfg.saltFile}') as file:
|
||||
SALT = file.read()
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.powerdns-admin = {
|
||||
enable = mkEnableOption "the PowerDNS web interface";
|
||||
|
||||
extraArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = literalExpression ''
|
||||
[ "-b" "127.0.0.1:8000" ]
|
||||
'';
|
||||
description = ''
|
||||
Extra arguments passed to powerdns-admin.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = ''
|
||||
BIND_ADDRESS = '127.0.0.1'
|
||||
PORT = 8000
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://powerdnsadmin@/powerdnsadmin?host=/run/postgresql'
|
||||
'';
|
||||
description = ''
|
||||
Configuration python file.
|
||||
See <link xlink:href="https://github.com/ngoduykhanh/PowerDNS-Admin/blob/v${pkgs.powerdns-admin.version}/configs/development.py">the example configuration</link>
|
||||
for options.
|
||||
'';
|
||||
};
|
||||
|
||||
secretKeyFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
example = "/etc/powerdns-admin/secret";
|
||||
description = ''
|
||||
The secret used to create cookies.
|
||||
This needs to be set, otherwise the default is used and everyone can forge valid login cookies.
|
||||
Set this to null to ignore this setting and configure it through another way.
|
||||
'';
|
||||
};
|
||||
|
||||
saltFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
example = "/etc/powerdns-admin/salt";
|
||||
description = ''
|
||||
The salt used for serialization.
|
||||
This should be set, otherwise the default is used.
|
||||
Set this to null to ignore this setting and configure it through another way.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.powerdns-admin = {
|
||||
description = "PowerDNS web interface";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
|
||||
environment.FLASK_CONF = builtins.toFile "powerdns-admin-config.py" configText;
|
||||
environment.PYTHONPATH = pkgs.powerdns-admin.pythonPath;
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.powerdns-admin}/bin/powerdns-admin --pid /run/powerdns-admin/pid ${escapeShellArgs cfg.extraArgs}";
|
||||
ExecStartPre = "${pkgs.coreutils}/bin/env FLASK_APP=${pkgs.powerdns-admin}/share/powerdnsadmin/__init__.py ${pkgs.python3Packages.flask}/bin/flask db upgrade -d ${pkgs.powerdns-admin}/share/migrations";
|
||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||
ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
|
||||
PIDFile = "/run/powerdns-admin/pid";
|
||||
RuntimeDirectory = "powerdns-admin";
|
||||
User = "powerdnsadmin";
|
||||
Group = "powerdnsadmin";
|
||||
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
]
|
||||
++ (optional (cfg.secretKeyFile != null) cfg.secretKeyFile)
|
||||
++ (optional (cfg.saltFile != null) cfg.saltFile);
|
||||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||||
# ProtectClock= adds DeviceAllow=char-rtc r
|
||||
DeviceAllow = "";
|
||||
# Implies ProtectSystem=strict, which re-mounts all paths
|
||||
#DynamicUser = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
# Needs to start a server
|
||||
#PrivateNetwork = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
# gunicorn needs setuid
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged @resources @keyring"
|
||||
# These got removed by the line above but are needed
|
||||
"@setuid @chown"
|
||||
];
|
||||
TemporaryFileSystem = "/:ro";
|
||||
# Does not work well with the temporary root
|
||||
#UMask = "0066";
|
||||
};
|
||||
};
|
||||
|
||||
users.groups.powerdnsadmin = { };
|
||||
users.users.powerdnsadmin = {
|
||||
description = "PowerDNS web interface user";
|
||||
isSystemUser = true;
|
||||
group = "powerdnsadmin";
|
||||
};
|
||||
};
|
||||
|
||||
# uses attributes of the linked package
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
86
nixos/modules/services/web-apps/prosody-filer.nix
Normal file
86
nixos/modules/services/web-apps/prosody-filer.nix
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
|
||||
cfg = config.services.prosody-filer;
|
||||
|
||||
settingsFormat = pkgs.formats.toml { };
|
||||
configFile = settingsFormat.generate "prosody-filer.toml" cfg.settings;
|
||||
in {
|
||||
|
||||
options = {
|
||||
services.prosody-filer = {
|
||||
enable = mkEnableOption "Prosody Filer XMPP upload file server";
|
||||
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Configuration for Prosody Filer.
|
||||
Refer to <link xlink:href="https://github.com/ThomasLeister/prosody-filer#configure-prosody-filer"/> for details on supported values.
|
||||
'';
|
||||
|
||||
type = settingsFormat.type;
|
||||
|
||||
example = {
|
||||
secret = "mysecret";
|
||||
storeDir = "/srv/http/nginx/prosody-upload";
|
||||
};
|
||||
|
||||
defaultText = literalExpression ''
|
||||
{
|
||||
listenport = mkDefault "127.0.0.1:5050";
|
||||
uploadSubDir = mkDefault "upload/";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.prosody-filer.settings = {
|
||||
listenport = mkDefault "127.0.0.1:5050";
|
||||
uploadSubDir = mkDefault "upload/";
|
||||
};
|
||||
|
||||
users.users.prosody-filer = {
|
||||
group = "prosody-filer";
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
users.groups.prosody-filer = { };
|
||||
|
||||
systemd.services.prosody-filer = {
|
||||
description = "Prosody file upload server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
User = "prosody-filer";
|
||||
Group = "prosody-filer";
|
||||
ExecStart = "${pkgs.prosody-filer}/bin/prosody-filer -config ${configFile}";
|
||||
Restart = "on-failure";
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateMounts = true;
|
||||
ProtectHome = true;
|
||||
ProtectClock = true;
|
||||
ProtectProc = "noaccess";
|
||||
ProcSubset = "pid";
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHostname = true;
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictNamespaces = true;
|
||||
LockPersonality = true;
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
380
nixos/modules/services/web-apps/restya-board.nix
Normal file
380
nixos/modules/services/web-apps/restya-board.nix
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
# TODO: are these php-packages needed?
|
||||
#imagick
|
||||
#php-geoip -> php.ini: extension = geoip.so
|
||||
#expat
|
||||
|
||||
let
|
||||
cfg = config.services.restya-board;
|
||||
fpm = config.services.phpfpm.pools.${poolName};
|
||||
|
||||
runDir = "/run/restya-board";
|
||||
|
||||
poolName = "restya-board";
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.restya-board = {
|
||||
|
||||
enable = mkEnableOption "restya-board";
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/restya-board";
|
||||
description = ''
|
||||
Data of the application.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "restya-board";
|
||||
description = ''
|
||||
User account under which the web-application runs.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "nginx";
|
||||
description = ''
|
||||
Group account under which the web-application runs.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualHost = {
|
||||
serverName = mkOption {
|
||||
type = types.str;
|
||||
default = "restya.board";
|
||||
description = ''
|
||||
Name of the nginx virtualhost to use.
|
||||
'';
|
||||
};
|
||||
|
||||
listenHost = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Listen address for the virtualhost to use.
|
||||
'';
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.int;
|
||||
default = 3000;
|
||||
description = ''
|
||||
Listen port for the virtualhost to use.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Host of the database. Leave 'null' to use a local PostgreSQL database.
|
||||
A local PostgreSQL database is initialized automatically.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.int;
|
||||
default = 5432;
|
||||
description = ''
|
||||
The database's port.
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "restya_board";
|
||||
description = ''
|
||||
Name of the database. The database must exist.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "restya_board";
|
||||
description = ''
|
||||
The database user. The user must exist and have access to
|
||||
the specified database.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The database user's password. 'null' if no password is set.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
email = {
|
||||
server = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "localhost";
|
||||
description = ''
|
||||
Hostname to send outgoing mail. Null to use the system MTA.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 25;
|
||||
description = ''
|
||||
Port used to connect to SMTP server.
|
||||
'';
|
||||
};
|
||||
|
||||
login = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
SMTP authentication login used when sending outgoing mail.
|
||||
'';
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
SMTP authentication password used when sending outgoing mail.
|
||||
|
||||
ATTENTION: The password is stored world-readable in the nix-store!
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
timezone = mkOption {
|
||||
type = types.lines;
|
||||
default = "GMT";
|
||||
description = ''
|
||||
Timezone the web-app runs in.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.phpfpm.pools = {
|
||||
${poolName} = {
|
||||
inherit (cfg) user group;
|
||||
|
||||
phpOptions = ''
|
||||
date.timezone = "CET"
|
||||
|
||||
${optionalString (cfg.email.server != null) ''
|
||||
SMTP = ${cfg.email.server}
|
||||
smtp_port = ${toString cfg.email.port}
|
||||
auth_username = ${cfg.email.login}
|
||||
auth_password = ${cfg.email.password}
|
||||
''}
|
||||
'';
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0600";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 10;
|
||||
"pm.min_spare_servers" = 5;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx.enable = true;
|
||||
services.nginx.virtualHosts.${cfg.virtualHost.serverName} = {
|
||||
listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ];
|
||||
serverName = cfg.virtualHost.serverName;
|
||||
root = runDir;
|
||||
extraConfig = ''
|
||||
index index.html index.php;
|
||||
|
||||
gzip on;
|
||||
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1100;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_proxied any;
|
||||
gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss;
|
||||
|
||||
client_max_body_size 300M;
|
||||
|
||||
rewrite ^/oauth/authorize$ /server/php/authorize.php last;
|
||||
rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last;
|
||||
rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last;
|
||||
rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last;
|
||||
rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last;
|
||||
rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last;
|
||||
'';
|
||||
|
||||
locations."/".root = "${runDir}/client";
|
||||
|
||||
locations."~ \\.php$" = {
|
||||
tryFiles = "$uri =404";
|
||||
extraConfig = ''
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
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";
|
||||
'';
|
||||
};
|
||||
|
||||
locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = {
|
||||
root = "${runDir}/client";
|
||||
extraConfig = ''
|
||||
if (-f $request_filename) {
|
||||
break;
|
||||
}
|
||||
rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last;
|
||||
add_header Cache-Control public;
|
||||
add_header Cache-Control must-revalidate;
|
||||
expires 7d;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restya-board-init = {
|
||||
description = "Restya board initialization";
|
||||
serviceConfig.Type = "oneshot";
|
||||
serviceConfig.RemainAfterExit = true;
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = if cfg.database.host == null then [] else [ "postgresql.service" ];
|
||||
after = [ "network.target" ] ++ (if cfg.database.host == null then [] else [ "postgresql.service" ]);
|
||||
|
||||
script = ''
|
||||
rm -rf "${runDir}"
|
||||
mkdir -m 750 -p "${runDir}"
|
||||
cp -r "${pkgs.restya-board}/"* "${runDir}"
|
||||
sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql"
|
||||
rm -rf "${runDir}/media"
|
||||
rm -rf "${runDir}/client/img"
|
||||
chmod -R 0750 "${runDir}"
|
||||
|
||||
sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh
|
||||
|
||||
${if (cfg.database.host == null) then ''
|
||||
sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php"
|
||||
sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php"
|
||||
'' else ''
|
||||
sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php"
|
||||
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"
|
||||
''}
|
||||
sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php"
|
||||
sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php"
|
||||
sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php"
|
||||
|
||||
chmod 0400 "${runDir}/server/php/config.inc.php"
|
||||
|
||||
ln -sf "${cfg.dataDir}/media" "${runDir}/media"
|
||||
ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img"
|
||||
|
||||
chmod g+w "${runDir}/tmp/cache"
|
||||
chown -R "${cfg.user}":"${cfg.group}" "${runDir}"
|
||||
|
||||
|
||||
mkdir -m 0750 -p "${cfg.dataDir}"
|
||||
mkdir -m 0750 -p "${cfg.dataDir}/media"
|
||||
mkdir -m 0750 -p "${cfg.dataDir}/client/img"
|
||||
cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media"
|
||||
cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img"
|
||||
chown "${cfg.user}":"${cfg.group}" "${cfg.dataDir}"
|
||||
chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/media"
|
||||
chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/client/img"
|
||||
|
||||
${optionalString (cfg.database.host == null) ''
|
||||
if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then
|
||||
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
|
||||
${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
|
||||
-c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'"
|
||||
|
||||
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \
|
||||
${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \
|
||||
-c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0"
|
||||
|
||||
${pkgs.sudo}/bin/sudo -u ${cfg.user} \
|
||||
${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \
|
||||
-d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql"
|
||||
|
||||
touch "${cfg.dataDir}/.db-initialized"
|
||||
fi
|
||||
''}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.restya-board = {
|
||||
description = "restya-board scripts for e.g. email notification";
|
||||
wantedBy = [ "timers.target" ];
|
||||
after = [ "restya-board-init.service" ];
|
||||
requires = [ "restya-board-init.service" ];
|
||||
timerConfig = {
|
||||
OnUnitInactiveSec = "60s";
|
||||
Unit = "restya-board-timers.service";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.restya-board-timers = {
|
||||
description = "restya-board scripts for e.g. email notification";
|
||||
serviceConfig.Type = "oneshot";
|
||||
serviceConfig.User = cfg.user;
|
||||
|
||||
after = [ "restya-board-init.service" ];
|
||||
requires = [ "restya-board-init.service" ];
|
||||
|
||||
script = ''
|
||||
/bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true
|
||||
/bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true
|
||||
/bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true
|
||||
/bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true
|
||||
/bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true
|
||||
'';
|
||||
};
|
||||
|
||||
users.users.restya-board = {
|
||||
isSystemUser = true;
|
||||
createHome = false;
|
||||
home = runDir;
|
||||
group = "restya-board";
|
||||
};
|
||||
users.groups.restya-board = {};
|
||||
|
||||
services.postgresql.enable = mkIf (cfg.database.host == null) true;
|
||||
|
||||
services.postgresql.identMap = optionalString (cfg.database.host == null)
|
||||
''
|
||||
restya-board-users restya-board restya_board
|
||||
'';
|
||||
|
||||
services.postgresql.authentication = optionalString (cfg.database.host == null)
|
||||
''
|
||||
local restya_board all ident map=restya-board-users
|
||||
'';
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
125
nixos/modules/services/web-apps/rss-bridge.nix
Normal file
125
nixos/modules/services/web-apps/rss-bridge.nix
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.rss-bridge;
|
||||
|
||||
poolName = "rss-bridge";
|
||||
|
||||
whitelist = pkgs.writeText "rss-bridge_whitelist.txt"
|
||||
(concatStringsSep "\n" cfg.whitelist);
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.rss-bridge = {
|
||||
enable = mkEnableOption "rss-bridge";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "nginx";
|
||||
description = ''
|
||||
User account under which both the service and the web-application run.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "nginx";
|
||||
description = ''
|
||||
Group under which the web-application run.
|
||||
'';
|
||||
};
|
||||
|
||||
pool = mkOption {
|
||||
type = types.str;
|
||||
default = poolName;
|
||||
description = ''
|
||||
Name of existing phpfpm pool that is used to run web-application.
|
||||
If not specified a pool will be created automatically with
|
||||
default values.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/rss-bridge";
|
||||
description = ''
|
||||
Location in which cache directory will be created.
|
||||
You can put <literal>config.ini.php</literal> in here.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "rss-bridge";
|
||||
description = ''
|
||||
Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
|
||||
'';
|
||||
};
|
||||
|
||||
whitelist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = options.literalExpression ''
|
||||
[
|
||||
"Facebook"
|
||||
"Instagram"
|
||||
"Twitter"
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
List of bridges to be whitelisted.
|
||||
If the list is empty, rss-bridge will use whitelist.default.txt.
|
||||
Use <literal>[ "*" ]</literal> to whitelist all.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.phpfpm.pools = mkIf (cfg.pool == poolName) {
|
||||
${poolName} = {
|
||||
user = cfg.user;
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = cfg.user;
|
||||
"listen.group" = cfg.user;
|
||||
"listen.mode" = "0600";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 10;
|
||||
"pm.min_spare_servers" = 5;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.dataDir}/cache' 0750 ${cfg.user} ${cfg.group} - -"
|
||||
(mkIf (cfg.whitelist != []) "L+ ${cfg.dataDir}/whitelist.txt - - - - ${whitelist}")
|
||||
"z '${cfg.dataDir}/config.ini.php' 0750 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
services.nginx = mkIf (cfg.virtualHost != null) {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
${cfg.virtualHost} = {
|
||||
root = "${pkgs.rss-bridge}";
|
||||
|
||||
locations."/" = {
|
||||
tryFiles = "$uri /index.php$is_args$args";
|
||||
};
|
||||
|
||||
locations."~ ^/index.php(/|$)" = {
|
||||
extraConfig = ''
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param RSSBRIDGE_DATA ${cfg.dataDir};
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
164
nixos/modules/services/web-apps/selfoss.nix
Normal file
164
nixos/modules/services/web-apps/selfoss.nix
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.selfoss;
|
||||
|
||||
poolName = "selfoss_pool";
|
||||
|
||||
dataDir = "/var/lib/selfoss";
|
||||
|
||||
selfoss-config =
|
||||
let
|
||||
db_type = cfg.database.type;
|
||||
default_port = if (db_type == "mysql") then 3306 else 5342;
|
||||
in
|
||||
pkgs.writeText "selfoss-config.ini" ''
|
||||
[globals]
|
||||
${lib.optionalString (db_type != "sqlite") ''
|
||||
db_type=${db_type}
|
||||
db_host=${cfg.database.host}
|
||||
db_database=${cfg.database.name}
|
||||
db_username=${cfg.database.user}
|
||||
db_password=${cfg.database.password}
|
||||
db_port=${toString (if (cfg.database.port != null) then cfg.database.port
|
||||
else default_port)}
|
||||
''
|
||||
}
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.selfoss = {
|
||||
enable = mkEnableOption "selfoss";
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "nginx";
|
||||
description = ''
|
||||
User account under which both the service and the web-application run.
|
||||
'';
|
||||
};
|
||||
|
||||
pool = mkOption {
|
||||
type = types.str;
|
||||
default = "${poolName}";
|
||||
description = ''
|
||||
Name of existing phpfpm pool that is used to run web-application.
|
||||
If not specified a pool will be created automatically with
|
||||
default values.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum ["pgsql" "mysql" "sqlite"];
|
||||
default = "sqlite";
|
||||
description = ''
|
||||
Database to store feeds. Supported are sqlite, pgsql and mysql.
|
||||
'';
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Host of the database (has no effect if type is "sqlite").
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "tt_rss";
|
||||
description = ''
|
||||
Name of the existing database (has no effect if type is "sqlite").
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "tt_rss";
|
||||
description = ''
|
||||
The database user. The user must exist and has access to
|
||||
the specified database (has no effect if type is "sqlite").
|
||||
'';
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The database user's password (has no effect if type is "sqlite").
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
The database's port. If not set, the default ports will be
|
||||
provided (5432 and 3306 for pgsql and mysql respectively)
|
||||
(has no effect if type is "sqlite").
|
||||
'';
|
||||
};
|
||||
};
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Extra configuration added to config.ini
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
|
||||
${poolName} = {
|
||||
user = "nginx";
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0600";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 10;
|
||||
"pm.min_spare_servers" = 5;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.selfoss-config = {
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
mkdir -m 755 -p ${dataDir}
|
||||
cd ${dataDir}
|
||||
|
||||
# Delete all but the "data" folder
|
||||
ls | grep -v data | while read line; do rm -rf $line; done || true
|
||||
|
||||
# Create the files
|
||||
cp -r "${pkgs.selfoss}/"* "${dataDir}"
|
||||
ln -sf "${selfoss-config}" "${dataDir}/config.ini"
|
||||
chown -R "${cfg.user}" "${dataDir}"
|
||||
chmod -R 755 "${dataDir}"
|
||||
'';
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
|
||||
systemd.services.selfoss-update = {
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.php}/bin/php ${dataDir}/cliupdate.php";
|
||||
User = "${cfg.user}";
|
||||
};
|
||||
startAt = "hourly";
|
||||
after = [ "selfoss-config.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
96
nixos/modules/services/web-apps/shiori.nix
Normal file
96
nixos/modules/services/web-apps/shiori.nix
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.shiori;
|
||||
in {
|
||||
options = {
|
||||
services.shiori = {
|
||||
enable = mkEnableOption "Shiori simple bookmarks manager";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.shiori;
|
||||
defaultText = literalExpression "pkgs.shiori";
|
||||
description = "The Shiori package to use.";
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
The IP address on which Shiori will listen.
|
||||
If empty, listens on all interfaces.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8080;
|
||||
description = "The port of the Shiori web application";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.shiori = with cfg; {
|
||||
description = "Shiori simple bookmarks manager";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment.SHIORI_DIR = "/var/lib/shiori";
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'";
|
||||
|
||||
DynamicUser = true;
|
||||
StateDirectory = "shiori";
|
||||
# As the RootDirectory
|
||||
RuntimeDirectory = "shiori";
|
||||
|
||||
# Security options
|
||||
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
|
||||
# For SSL certificates, and the resolv.conf
|
||||
"/etc"
|
||||
];
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
|
||||
DeviceAllow = "";
|
||||
|
||||
LockPersonality = true;
|
||||
|
||||
MemoryDenyWriteExecute = true;
|
||||
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
|
||||
RestrictNamespaces = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
|
||||
RootDirectory = "/run/shiori";
|
||||
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallErrorNumber = "EPERM";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@resources" "~@setuid"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ minijackson ];
|
||||
}
|
||||
493
nixos/modules/services/web-apps/snipe-it.nix
Normal file
493
nixos/modules/services/web-apps/snipe-it.nix
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.snipe-it;
|
||||
snipe-it = pkgs.snipe-it.override {
|
||||
dataDir = cfg.dataDir;
|
||||
};
|
||||
db = cfg.database;
|
||||
mail = cfg.mail;
|
||||
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
|
||||
tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
|
||||
|
||||
# shell script for local administration
|
||||
artisan = pkgs.writeScriptBin "snipe-it" ''
|
||||
#! ${pkgs.runtimeShell}
|
||||
cd ${snipe-it}
|
||||
sudo=exec
|
||||
if [[ "$USER" != ${user} ]]; then
|
||||
sudo='exec /run/wrappers/bin/sudo -u ${user}'
|
||||
fi
|
||||
$sudo ${pkgs.php}/bin/php artisan $*
|
||||
'';
|
||||
in {
|
||||
options.services.snipe-it = {
|
||||
|
||||
enable = mkEnableOption "A free open source IT asset/license management system";
|
||||
|
||||
user = mkOption {
|
||||
default = "snipeit";
|
||||
description = "User snipe-it runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
default = "snipeit";
|
||||
description = "Group snipe-it runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
appKeyFile = mkOption {
|
||||
description = ''
|
||||
A file containing the Laravel APP_KEY - a 32 character long,
|
||||
base64 encoded key used for encryption where needed. Can be
|
||||
generated with <code>head -c 32 /dev/urandom | base64</code>.
|
||||
'';
|
||||
example = "/run/keys/snipe-it/appkey";
|
||||
type = types.path;
|
||||
};
|
||||
|
||||
hostName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = if config.networking.domain != null then
|
||||
config.networking.fqdn
|
||||
else
|
||||
config.networking.hostName;
|
||||
defaultText = lib.literalExpression "config.networking.fqdn";
|
||||
example = "snipe-it.example.com";
|
||||
description = ''
|
||||
The hostname to serve Snipe-IT on.
|
||||
'';
|
||||
};
|
||||
|
||||
appURL = mkOption {
|
||||
description = ''
|
||||
The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
|
||||
If you change this in the future you may need to run a command to update stored URLs in the database.
|
||||
Command example: <code>snipe-it snipe-it:update-url https://old.example.com https://new.example.com</code>
|
||||
'';
|
||||
default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
|
||||
defaultText = ''
|
||||
http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
|
||||
'';
|
||||
example = "https://example.com";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
description = "snipe-it data directory";
|
||||
default = "/var/lib/snipe-it";
|
||||
type = types.path;
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = "Database host port.";
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "snipeit";
|
||||
description = "Database name.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = user;
|
||||
defaultText = literalExpression "user";
|
||||
description = "Database username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/snipe-it/dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
mail = {
|
||||
driver = mkOption {
|
||||
type = types.enum [ "smtp" "sendmail" ];
|
||||
default = "smtp";
|
||||
description = "Mail driver to use.";
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Mail host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 1025;
|
||||
description = "Mail host port.";
|
||||
};
|
||||
encryption = mkOption {
|
||||
type = with types; nullOr (enum [ "tls" "ssl" ]);
|
||||
default = null;
|
||||
description = "SMTP encryption mechanism to use.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "snipeit";
|
||||
description = "Mail username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/snipe-it/mailpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>mail.user</option>.
|
||||
'';
|
||||
};
|
||||
backupNotificationAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "backup@example.com";
|
||||
description = "Email Address to send Backup Notifications to.";
|
||||
};
|
||||
from = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "Snipe-IT Asset Management";
|
||||
description = "Mail \"from\" name.";
|
||||
};
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "mail@example.com";
|
||||
description = "Mail \"from\" address.";
|
||||
};
|
||||
};
|
||||
replyTo = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "Snipe-IT Asset Management";
|
||||
description = "Mail \"reply-to\" name.";
|
||||
};
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "mail@example.com";
|
||||
description = "Mail \"reply-to\" address.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
maxUploadSize = mkOption {
|
||||
type = types.str;
|
||||
default = "18M";
|
||||
example = "1G";
|
||||
description = "The maximum size for uploads (e.g. images).";
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the snipe-it PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
type = types.submodule (
|
||||
recursiveUpdate
|
||||
(import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
|
||||
);
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
serverAliases = [
|
||||
"snipe-it.''${config.networking.domain}"
|
||||
];
|
||||
# To enable encryption and let let's encrypt take care of certificate
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
With this option, you can customize the nginx virtualHost settings.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types;
|
||||
attrsOf
|
||||
(nullOr
|
||||
(either
|
||||
(oneOf [
|
||||
bool
|
||||
int
|
||||
port
|
||||
path
|
||||
str
|
||||
])
|
||||
(submodule {
|
||||
options = {
|
||||
_secret = mkOption {
|
||||
type = nullOr (oneOf [ str path ]);
|
||||
description = ''
|
||||
The path to a file containing the value the
|
||||
option should be set to in the final
|
||||
configuration file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
})));
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
ALLOWED_IFRAME_HOSTS = "https://example.com";
|
||||
WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
|
||||
AUTH_METHOD = "oidc";
|
||||
OIDC_NAME = "MyLogin";
|
||||
OIDC_DISPLAY_NAME_CLAIMS = "name";
|
||||
OIDC_CLIENT_ID = "snipe-it";
|
||||
OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
|
||||
OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
|
||||
OIDC_ISSUER_DISCOVER = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Snipe-IT configuration options to set in the
|
||||
<filename>.env</filename> file.
|
||||
Refer to <link xlink:href="https://snipe-it.readme.io/docs/configuration"/>
|
||||
for details on supported values.
|
||||
|
||||
Settings containing secret data should be set to an attribute
|
||||
set containing the attribute <literal>_secret</literal> - a
|
||||
string pointing to a file containing the value the option
|
||||
should be set to. See the example to get a better picture of
|
||||
this: in the resulting <filename>.env</filename> file, the
|
||||
<literal>OIDC_CLIENT_SECRET</literal> key will be set to the
|
||||
contents of the <filename>/run/keys/oidc_secret</filename>
|
||||
file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = db.createLocally -> db.user == user;
|
||||
message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
|
||||
}
|
||||
{ assertion = db.createLocally -> db.passwordFile == null;
|
||||
message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = [ artisan ];
|
||||
|
||||
services.snipe-it.config = {
|
||||
APP_ENV = "production";
|
||||
APP_KEY._secret = cfg.appKeyFile;
|
||||
APP_URL = cfg.appURL;
|
||||
DB_HOST = db.host;
|
||||
DB_PORT = db.port;
|
||||
DB_DATABASE = db.name;
|
||||
DB_USERNAME = db.user;
|
||||
DB_PASSWORD._secret = db.passwordFile;
|
||||
MAIL_DRIVER = mail.driver;
|
||||
MAIL_FROM_NAME = mail.from.name;
|
||||
MAIL_FROM_ADDR = mail.from.address;
|
||||
MAIL_REPLYTO_NAME = mail.from.name;
|
||||
MAIL_REPLYTO_ADDR = mail.from.address;
|
||||
MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
|
||||
MAIL_HOST = mail.host;
|
||||
MAIL_PORT = mail.port;
|
||||
MAIL_USERNAME = mail.user;
|
||||
MAIL_ENCRYPTION = mail.encryption;
|
||||
MAIL_PASSWORD._secret = mail.passwordFile;
|
||||
APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
|
||||
APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
|
||||
APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
|
||||
APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
|
||||
APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
|
||||
SESSION_SECURE_COOKIE = tlsEnabled;
|
||||
};
|
||||
|
||||
services.mysql = mkIf db.createLocally {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ db.name ];
|
||||
ensureUsers = [
|
||||
{ name = db.user;
|
||||
ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.snipe-it = {
|
||||
inherit user group;
|
||||
phpPackage = pkgs.php74;
|
||||
phpOptions = ''
|
||||
post_max_size = ${cfg.maxUploadSize}
|
||||
upload_max_filesize = ${cfg.maxUploadSize}
|
||||
'';
|
||||
settings = {
|
||||
"listen.mode" = "0660";
|
||||
"listen.owner" = user;
|
||||
"listen.group" = group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = mkDefault true;
|
||||
virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
|
||||
root = mkForce "${snipe-it}/public";
|
||||
extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
|
||||
locations = {
|
||||
"/" = {
|
||||
index = "index.php";
|
||||
extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
|
||||
};
|
||||
"~ \.php$" = {
|
||||
extraConfig = ''
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param REDIRECT_STATUS 200;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
|
||||
${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
|
||||
'';
|
||||
};
|
||||
"~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
|
||||
extraConfig = "expires 365d;";
|
||||
};
|
||||
};
|
||||
}];
|
||||
};
|
||||
|
||||
systemd.services.snipe-it-setup = {
|
||||
description = "Preperation tasks for snipe-it";
|
||||
before = [ "phpfpm-snipe-it.service" ];
|
||||
after = optional db.createLocally "mysql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = user;
|
||||
WorkingDirectory = snipe-it;
|
||||
RuntimeDirectory = "snipe-it/cache";
|
||||
RuntimeDirectoryMode = 0700;
|
||||
};
|
||||
path = [ pkgs.replace-secret ];
|
||||
script =
|
||||
let
|
||||
isSecret = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
|
||||
snipeITEnvVars = lib.generators.toKeyValue {
|
||||
mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
|
||||
mkValueString = v: with builtins;
|
||||
if isInt v then toString v
|
||||
else if isString v then "\"${v}\""
|
||||
else if true == v then "true"
|
||||
else if false == v then "false"
|
||||
else if isSecret v then
|
||||
if (isString v._secret) then
|
||||
hashString "sha256" v._secret
|
||||
else
|
||||
hashString "sha256" (builtins.readFile v._secret)
|
||||
else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
|
||||
};
|
||||
};
|
||||
secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
|
||||
mkSecretReplacement = file: ''
|
||||
replace-secret ${escapeShellArgs [
|
||||
(
|
||||
if (isString file) then
|
||||
builtins.hashString "sha256" file
|
||||
else
|
||||
builtins.hashString "sha256" (builtins.readFile file)
|
||||
)
|
||||
file
|
||||
"${cfg.dataDir}/.env"
|
||||
]}
|
||||
'';
|
||||
secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
|
||||
filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
|
||||
snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
|
||||
in ''
|
||||
# error handling
|
||||
set -euo pipefail
|
||||
|
||||
# set permissions
|
||||
umask 077
|
||||
|
||||
# create .env file
|
||||
install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
|
||||
|
||||
# replace secrets
|
||||
${secretReplacements}
|
||||
|
||||
# prepend `base64:` if it does not exist in APP_KEY
|
||||
if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
|
||||
sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
|
||||
fi
|
||||
|
||||
# purge cache
|
||||
rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
|
||||
|
||||
# migrate db
|
||||
${pkgs.php}/bin/php artisan migrate --force
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0710 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/bootstrap 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/bootstrap/cache 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/public 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -"
|
||||
"d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -"
|
||||
];
|
||||
|
||||
users = {
|
||||
users = mkIf (user == "snipeit") {
|
||||
snipeit = {
|
||||
inherit group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
"${config.services.nginx.user}".extraGroups = [ group ];
|
||||
};
|
||||
groups = mkIf (group == "snipeit") {
|
||||
snipeit = {};
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ yayayayaka ];
|
||||
}
|
||||
271
nixos/modules/services/web-apps/sogo.nix
Normal file
271
nixos/modules/services/web-apps/sogo.nix
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
{ config, pkgs, lib, ... }: with lib; let
|
||||
cfg = config.services.sogo;
|
||||
|
||||
preStart = pkgs.writeShellScriptBin "sogo-prestart" ''
|
||||
touch /etc/sogo/sogo.conf
|
||||
chown sogo:sogo /etc/sogo/sogo.conf
|
||||
chmod 640 /etc/sogo/sogo.conf
|
||||
|
||||
${if (cfg.configReplaces != {}) then ''
|
||||
# Insert secrets
|
||||
${concatStringsSep "\n" (mapAttrsToList (k: v: ''export ${k}="$(cat "${v}" | tr -d '\n')"'') cfg.configReplaces)}
|
||||
|
||||
${pkgs.perl}/bin/perl -p ${concatStringsSep " " (mapAttrsToList (k: v: '' -e 's/${k}/''${ENV{"${k}"}}/g;' '') cfg.configReplaces)} /etc/sogo/sogo.conf.raw > /etc/sogo/sogo.conf
|
||||
'' else ''
|
||||
cp /etc/sogo/sogo.conf.raw /etc/sogo/sogo.conf
|
||||
''}
|
||||
'';
|
||||
|
||||
in {
|
||||
options.services.sogo = with types; {
|
||||
enable = mkEnableOption "SOGo groupware";
|
||||
|
||||
vhostName = mkOption {
|
||||
description = "Name of the nginx vhost";
|
||||
type = str;
|
||||
default = "sogo";
|
||||
};
|
||||
|
||||
timezone = mkOption {
|
||||
description = "Timezone of your SOGo instance";
|
||||
type = str;
|
||||
example = "America/Montreal";
|
||||
};
|
||||
|
||||
language = mkOption {
|
||||
description = "Language of SOGo";
|
||||
type = str;
|
||||
default = "English";
|
||||
};
|
||||
|
||||
ealarmsCredFile = mkOption {
|
||||
description = "Optional path to a credentials file for email alarms";
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
configReplaces = mkOption {
|
||||
description = ''
|
||||
Replacement-filepath mapping for sogo.conf.
|
||||
Every key is replaced with the contents of the file specified as value.
|
||||
|
||||
In the example, every occurence of LDAP_BINDPW will be replaced with the text of the
|
||||
specified file.
|
||||
'';
|
||||
type = attrsOf str;
|
||||
default = {};
|
||||
example = {
|
||||
LDAP_BINDPW = "/var/lib/secrets/sogo/ldappw";
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
description = "Extra sogo.conf configuration lines";
|
||||
type = lines;
|
||||
default = "";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.systemPackages = [ pkgs.sogo ];
|
||||
|
||||
environment.etc."sogo/sogo.conf.raw".text = ''
|
||||
{
|
||||
// Mandatory parameters
|
||||
SOGoTimeZone = "${cfg.timezone}";
|
||||
SOGoLanguage = "${cfg.language}";
|
||||
// Paths
|
||||
WOSendMail = "/run/wrappers/bin/sendmail";
|
||||
SOGoMailSpoolPath = "/var/lib/sogo/spool";
|
||||
// Enable CSRF protection
|
||||
SOGoXSRFValidationEnabled = YES;
|
||||
// Remove dates from log (jornald does that)
|
||||
NGLogDefaultLogEventFormatterClass = "NGLogEventFormatter";
|
||||
// Extra config
|
||||
${cfg.extraConfig}
|
||||
}
|
||||
'';
|
||||
|
||||
systemd.services.sogo = {
|
||||
description = "SOGo groupware";
|
||||
after = [ "postgresql.service" "mysql.service" "memcached.service" "openldap.service" "dovecot2.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
|
||||
|
||||
environment.LDAPTLS_CACERT = "/etc/ssl/certs/ca-certificates.crt";
|
||||
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
ExecStartPre = "+" + preStart + "/bin/sogo-prestart";
|
||||
ExecStart = "${pkgs.sogo}/bin/sogod -WOLogFile - -WOPidFile /run/sogo/sogo.pid";
|
||||
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
RuntimeDirectory = "sogo";
|
||||
StateDirectory = "sogo/spool";
|
||||
|
||||
User = "sogo";
|
||||
Group = "sogo";
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
PrivateMounts = true;
|
||||
PrivateUsers = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallFilter = "@basic-io @file-system @network-io @system-service @timer";
|
||||
SystemCallArchitectures = "native";
|
||||
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.sogo-tmpwatch = {
|
||||
description = "SOGo tmpwatch";
|
||||
|
||||
startAt = [ "hourly" ];
|
||||
script = ''
|
||||
SOGOSPOOL=/var/lib/sogo/spool
|
||||
|
||||
find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null
|
||||
find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
StateDirectory = "sogo/spool";
|
||||
|
||||
User = "sogo";
|
||||
Group = "sogo";
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
PrivateMounts = true;
|
||||
PrivateUsers = true;
|
||||
PrivateNetwork = true;
|
||||
SystemCallFilter = "@basic-io @file-system @system-service";
|
||||
SystemCallArchitectures = "native";
|
||||
RestrictAddressFamilies = "";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.sogo-ealarms = {
|
||||
description = "SOGo email alarms";
|
||||
|
||||
after = [ "postgresql.service" "mysqld.service" "memcached.service" "openldap.service" "dovecot2.service" "sogo.service" ];
|
||||
restartTriggers = [ config.environment.etc."sogo/sogo.conf.raw".source ];
|
||||
|
||||
startAt = [ "minutely" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.sogo}/bin/sogo-ealarms-notify${optionalString (cfg.ealarmsCredFile != null) " -p ${cfg.ealarmsCredFile}"}";
|
||||
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
StateDirectory = "sogo/spool";
|
||||
|
||||
User = "sogo";
|
||||
Group = "sogo";
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
NoNewPrivileges = true;
|
||||
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
PrivateMounts = true;
|
||||
PrivateUsers = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallFilter = "@basic-io @file-system @network-io @system-service";
|
||||
SystemCallArchitectures = "native";
|
||||
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
|
||||
};
|
||||
};
|
||||
|
||||
# nginx vhost
|
||||
services.nginx.virtualHosts."${cfg.vhostName}" = {
|
||||
locations."/".extraConfig = ''
|
||||
rewrite ^ https://$server_name/SOGo;
|
||||
allow all;
|
||||
'';
|
||||
|
||||
# For iOS 7
|
||||
locations."/principals/".extraConfig = ''
|
||||
rewrite ^ https://$server_name/SOGo/dav;
|
||||
allow all;
|
||||
'';
|
||||
|
||||
locations."^~/SOGo".extraConfig = ''
|
||||
proxy_pass http://127.0.0.1:20000;
|
||||
proxy_redirect http://127.0.0.1:20000 default;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-webobjects-server-protocol HTTP/1.0;
|
||||
proxy_set_header x-webobjects-remote-host 127.0.0.1;
|
||||
proxy_set_header x-webobjects-server-port $server_port;
|
||||
proxy_set_header x-webobjects-server-name $server_name;
|
||||
proxy_set_header x-webobjects-server-url $scheme://$host;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 90;
|
||||
proxy_read_timeout 90;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
proxy_temp_file_write_size 64k;
|
||||
client_max_body_size 50m;
|
||||
client_body_buffer_size 128k;
|
||||
break;
|
||||
'';
|
||||
|
||||
locations."/SOGo.woa/WebServerResources/".extraConfig = ''
|
||||
alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
|
||||
allow all;
|
||||
'';
|
||||
|
||||
locations."/SOGo/WebServerResources/".extraConfig = ''
|
||||
alias ${pkgs.sogo}/lib/GNUstep/SOGo/WebServerResources/;
|
||||
allow all;
|
||||
'';
|
||||
|
||||
locations."~ ^/SOGo/so/ControlPanel/Products/([^/]*)/Resources/(.*)$".extraConfig = ''
|
||||
alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
'';
|
||||
|
||||
locations."~ ^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\\.(jpg|png|gif|css|js)$".extraConfig = ''
|
||||
alias ${pkgs.sogo}/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
'';
|
||||
};
|
||||
|
||||
# User and group
|
||||
users.groups.sogo = {};
|
||||
users.users.sogo = {
|
||||
group = "sogo";
|
||||
isSystemUser = true;
|
||||
description = "SOGo service user";
|
||||
};
|
||||
};
|
||||
}
|
||||
146
nixos/modules/services/web-apps/trilium.nix
Normal file
146
nixos/modules/services/web-apps/trilium.nix
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.trilium-server;
|
||||
configIni = pkgs.writeText "trilium-config.ini" ''
|
||||
[General]
|
||||
# Instance name can be used to distinguish between different instances
|
||||
instanceName=${cfg.instanceName}
|
||||
|
||||
# Disable automatically generating desktop icon
|
||||
noDesktopIcon=true
|
||||
noBackup=${lib.boolToString cfg.noBackup}
|
||||
|
||||
[Network]
|
||||
# host setting is relevant only for web deployments - set the host on which the server will listen
|
||||
host=${cfg.host}
|
||||
# port setting is relevant only for web deployments, desktop builds run on random free port
|
||||
port=${toString cfg.port}
|
||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
|
||||
https=false
|
||||
'';
|
||||
in
|
||||
{
|
||||
|
||||
options.services.trilium-server = with lib; {
|
||||
enable = mkEnableOption "trilium-server";
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/trilium";
|
||||
description = ''
|
||||
The directory storing the notes database and the configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
instanceName = mkOption {
|
||||
type = types.str;
|
||||
default = "Trilium";
|
||||
description = ''
|
||||
Instance name used to distinguish between different instances
|
||||
'';
|
||||
};
|
||||
|
||||
noBackup = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Disable periodic database backups.
|
||||
'';
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = ''
|
||||
The host address to bind to (defaults to localhost).
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default = 8080;
|
||||
description = ''
|
||||
The port number to bind to.
|
||||
'';
|
||||
};
|
||||
|
||||
nginx = mkOption {
|
||||
default = {};
|
||||
description = ''
|
||||
Configuration for nginx reverse proxy.
|
||||
'';
|
||||
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Configure the nginx reverse proxy settings.
|
||||
'';
|
||||
};
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
The hostname use to setup the virtualhost configuration
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ fliegendewurst ];
|
||||
|
||||
users.groups.trilium = {};
|
||||
users.users.trilium = {
|
||||
description = "Trilium User";
|
||||
group = "trilium";
|
||||
home = cfg.dataDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
systemd.services.trilium-server = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment.TRILIUM_DATA_DIR = cfg.dataDir;
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.trilium-server}/bin/trilium-server";
|
||||
User = "trilium";
|
||||
Group = "trilium";
|
||||
PrivateTmp = "true";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d ${cfg.dataDir} 0750 trilium trilium - -"
|
||||
"L+ ${cfg.dataDir}/config.ini - - - - ${configIni}"
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
(lib.mkIf cfg.nginx.enable {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.nginx.hostName}" = {
|
||||
locations."/" = {
|
||||
proxyPass = "http://${cfg.host}:${toString cfg.port}/";
|
||||
extraConfig = ''
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
'';
|
||||
};
|
||||
extraConfig = ''
|
||||
client_max_body_size 0;
|
||||
'';
|
||||
};
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
686
nixos/modules/services/web-apps/tt-rss.nix
Normal file
686
nixos/modules/services/web-apps/tt-rss.nix
Normal file
|
|
@ -0,0 +1,686 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.tt-rss;
|
||||
|
||||
configVersion = 26;
|
||||
|
||||
dbPort = if cfg.database.port == null
|
||||
then (if cfg.database.type == "pgsql" then 5432 else 3306)
|
||||
else cfg.database.port;
|
||||
|
||||
poolName = "tt-rss";
|
||||
|
||||
mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
|
||||
pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
|
||||
|
||||
tt-rss-config = let
|
||||
password =
|
||||
if (cfg.database.password != null) then
|
||||
"'${(escape ["'" "\\"] cfg.database.password)}'"
|
||||
else if (cfg.database.passwordFile != null) then
|
||||
"file_get_contents('${cfg.database.passwordFile}')"
|
||||
else
|
||||
null
|
||||
;
|
||||
in pkgs.writeText "config.php" ''
|
||||
<?php
|
||||
putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php');
|
||||
|
||||
putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock');
|
||||
putenv('TTRSS_CACHE_DIR=${cfg.root}/cache');
|
||||
putenv('TTRSS_ICONS_DIR=${cfg.root}/feed-icons');
|
||||
putenv('TTRSS_ICONS_URL=feed-icons');
|
||||
putenv('TTRSS_SELF_URL_PATH=${cfg.selfUrlPath}');
|
||||
|
||||
putenv('TTRSS_MYSQL_CHARSET=UTF8');
|
||||
|
||||
putenv('TTRSS_DB_TYPE=${cfg.database.type}');
|
||||
putenv('TTRSS_DB_HOST=${optionalString (cfg.database.host != null) cfg.database.host}');
|
||||
putenv('TTRSS_DB_USER=${cfg.database.user}');
|
||||
putenv('TTRSS_DB_NAME=${cfg.database.name}');
|
||||
putenv('TTRSS_DB_PASS=' ${optionalString (password != null) ". ${password}"});
|
||||
putenv('TTRSS_DB_PORT=${toString dbPort}');
|
||||
|
||||
putenv('TTRSS_AUTH_AUTO_CREATE=${boolToString cfg.auth.autoCreate}');
|
||||
putenv('TTRSS_AUTH_AUTO_LOGIN=${boolToString cfg.auth.autoLogin}');
|
||||
|
||||
putenv('TTRSS_FEED_CRYPT_KEY=${escape ["'" "\\"] cfg.feedCryptKey}');
|
||||
|
||||
|
||||
putenv('TTRSS_SINGLE_USER_MODE=${boolToString cfg.singleUserMode}');
|
||||
|
||||
putenv('TTRSS_SIMPLE_UPDATE_MODE=${boolToString cfg.simpleUpdateMode}');
|
||||
|
||||
# Never check for updates - the running version of the code should
|
||||
# be controlled entirely by the version of TT-RSS active in the
|
||||
# current Nix profile. If TT-RSS updates itself to a version
|
||||
# requiring a database schema upgrade, and then the SystemD
|
||||
# tt-rss.service is restarted, the old code copied from the Nix
|
||||
# store will overwrite the updated version, causing the code to
|
||||
# detect the need for a schema "upgrade" (since the schema version
|
||||
# in the database is different than in the code), but the update
|
||||
# schema operation in TT-RSS will do nothing because the schema
|
||||
# version in the database is newer than that in the code.
|
||||
putenv('TTRSS_CHECK_FOR_UPDATES=false');
|
||||
|
||||
putenv('TTRSS_FORCE_ARTICLE_PURGE=${toString cfg.forceArticlePurge}');
|
||||
putenv('TTRSS_SESSION_COOKIE_LIFETIME=${toString cfg.sessionCookieLifetime}');
|
||||
putenv('TTRSS_ENABLE_GZIP_OUTPUT=${boolToString cfg.enableGZipOutput}');
|
||||
|
||||
putenv('TTRSS_PLUGINS=${builtins.concatStringsSep "," cfg.plugins}');
|
||||
|
||||
putenv('TTRSS_LOG_DESTINATION=${cfg.logDestination}');
|
||||
putenv('TTRSS_CONFIG_VERSION=${toString configVersion}');
|
||||
|
||||
|
||||
putenv('TTRSS_PUBSUBHUBBUB_ENABLED=${boolToString cfg.pubSubHubbub.enable}');
|
||||
putenv('TTRSS_PUBSUBHUBBUB_HUB=${cfg.pubSubHubbub.hub}');
|
||||
|
||||
putenv('TTRSS_SPHINX_SERVER=${cfg.sphinx.server}');
|
||||
putenv('TTRSS_SPHINX_INDEX=${builtins.concatStringsSep "," cfg.sphinx.index}');
|
||||
|
||||
putenv('TTRSS_ENABLE_REGISTRATION=${boolToString cfg.registration.enable}');
|
||||
putenv('TTRSS_REG_NOTIFY_ADDRESS=${cfg.registration.notifyAddress}');
|
||||
putenv('TTRSS_REG_MAX_USERS=${toString cfg.registration.maxUsers}');
|
||||
|
||||
putenv('TTRSS_SMTP_SERVER=${cfg.email.server}');
|
||||
putenv('TTRSS_SMTP_LOGIN=${cfg.email.login}');
|
||||
putenv('TTRSS_SMTP_PASSWORD=${escape ["'" "\\"] cfg.email.password}');
|
||||
putenv('TTRSS_SMTP_SECURE=${cfg.email.security}');
|
||||
|
||||
putenv('TTRSS_SMTP_FROM_NAME=${escape ["'" "\\"] cfg.email.fromName}');
|
||||
putenv('TTRSS_SMTP_FROM_ADDRESS=${escape ["'" "\\"] cfg.email.fromAddress}');
|
||||
putenv('TTRSS_DIGEST_SUBJECT=${escape ["'" "\\"] cfg.email.digestSubject}');
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
# tt-rss and plugins and themes and config.php
|
||||
servedRoot = pkgs.runCommand "tt-rss-served-root" {} ''
|
||||
cp --no-preserve=mode -r ${pkgs.tt-rss} $out
|
||||
cp ${tt-rss-config} $out/config.php
|
||||
${optionalString (cfg.pluginPackages != []) ''
|
||||
for plugin in ${concatStringsSep " " cfg.pluginPackages}; do
|
||||
cp -r "$plugin"/* "$out/plugins.local/"
|
||||
done
|
||||
''}
|
||||
${optionalString (cfg.themePackages != []) ''
|
||||
for theme in ${concatStringsSep " " cfg.themePackages}; do
|
||||
cp -r "$theme"/* "$out/themes.local/"
|
||||
done
|
||||
''}
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.tt-rss = {
|
||||
|
||||
enable = mkEnableOption "tt-rss";
|
||||
|
||||
root = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/tt-rss";
|
||||
description = ''
|
||||
Root of the application.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "tt_rss";
|
||||
description = ''
|
||||
User account under which both the update daemon and the web-application run.
|
||||
'';
|
||||
};
|
||||
|
||||
pool = mkOption {
|
||||
type = types.str;
|
||||
default = "${poolName}";
|
||||
description = ''
|
||||
Name of existing phpfpm pool that is used to run web-application.
|
||||
If not specified a pool will be created automatically with
|
||||
default values.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = "tt-rss";
|
||||
description = ''
|
||||
Name of the nginx virtualhost to use and setup. If null, do not setup any virtualhost.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum ["pgsql" "mysql"];
|
||||
default = "pgsql";
|
||||
description = ''
|
||||
Database to store feeds. Supported are pgsql and mysql.
|
||||
'';
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Host of the database. Leave null to use Unix domain socket.
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "tt_rss";
|
||||
description = ''
|
||||
Name of the existing database.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "tt_rss";
|
||||
description = ''
|
||||
The database user. The user must exist and has access to
|
||||
the specified database.
|
||||
'';
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The database user's password.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The database user's password.
|
||||
'';
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
The database's port. If not set, the default ports will be provided (5432
|
||||
and 3306 for pgsql and mysql respectively).
|
||||
'';
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
auth = {
|
||||
autoCreate = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Allow authentication modules to auto-create users in tt-rss internal
|
||||
database when authenticated successfully.
|
||||
'';
|
||||
};
|
||||
|
||||
autoLogin = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Automatically login user on remote or other kind of externally supplied
|
||||
authentication, otherwise redirect to login form as normal.
|
||||
If set to true, users won't be able to set application language
|
||||
and settings profile.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
pubSubHubbub = {
|
||||
hub = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
URL to a PubSubHubbub-compatible hub server. If defined, "Published
|
||||
articles" generated feed would automatically become PUSH-enabled.
|
||||
'';
|
||||
};
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss
|
||||
won't try to subscribe to PUSH feed updates.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sphinx = {
|
||||
server = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost:9312";
|
||||
description = ''
|
||||
Hostname:port combination for the Sphinx server.
|
||||
'';
|
||||
};
|
||||
|
||||
index = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = ["ttrss" "delta"];
|
||||
description = ''
|
||||
Index names in Sphinx configuration. Example configuration
|
||||
files are available on tt-rss wiki.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
registration = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Allow users to register themselves. Please be aware that allowing
|
||||
random people to access your tt-rss installation is a security risk
|
||||
and potentially might lead to data loss or server exploit. Disabled
|
||||
by default.
|
||||
'';
|
||||
};
|
||||
|
||||
notifyAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
Email address to send new user notifications to.
|
||||
'';
|
||||
};
|
||||
|
||||
maxUsers = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = ''
|
||||
Maximum amount of users which will be allowed to register on this
|
||||
system. 0 - no limit.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
email = {
|
||||
server = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "localhost:25";
|
||||
description = ''
|
||||
Hostname:port combination to send outgoing mail. Blank - use system
|
||||
MTA.
|
||||
'';
|
||||
};
|
||||
|
||||
login = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
SMTP authentication login used when sending outgoing mail.
|
||||
'';
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
SMTP authentication password used when sending outgoing mail.
|
||||
'';
|
||||
};
|
||||
|
||||
security = mkOption {
|
||||
type = types.enum ["" "ssl" "tls"];
|
||||
default = "";
|
||||
description = ''
|
||||
Used to select a secure SMTP connection. Allowed values: ssl, tls,
|
||||
or empty.
|
||||
'';
|
||||
};
|
||||
|
||||
fromName = mkOption {
|
||||
type = types.str;
|
||||
default = "Tiny Tiny RSS";
|
||||
description = ''
|
||||
Name for sending outgoing mail. This applies to password reset
|
||||
notifications, digest emails and any other mail.
|
||||
'';
|
||||
};
|
||||
|
||||
fromAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
Address for sending outgoing mail. This applies to password reset
|
||||
notifications, digest emails and any other mail.
|
||||
'';
|
||||
};
|
||||
|
||||
digestSubject = mkOption {
|
||||
type = types.str;
|
||||
default = "[tt-rss] New headlines for last 24 hours";
|
||||
description = ''
|
||||
Subject line for email digests.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
sessionCookieLifetime = mkOption {
|
||||
type = types.int;
|
||||
default = 86400;
|
||||
description = ''
|
||||
Default lifetime of a session (e.g. login) cookie. In seconds,
|
||||
0 means cookie will be deleted when browser closes.
|
||||
'';
|
||||
};
|
||||
|
||||
selfUrlPath = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Full URL of your tt-rss installation. This should be set to the
|
||||
location of tt-rss directory, e.g. http://example.org/tt-rss/
|
||||
You need to set this option correctly otherwise several features
|
||||
including PUSH, bookmarklets and browser integration will not work properly.
|
||||
'';
|
||||
example = "http://localhost";
|
||||
};
|
||||
|
||||
feedCryptKey = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
Key used for encryption of passwords for password-protected feeds
|
||||
in the database. A string of 24 random characters. If left blank, encryption
|
||||
is not used. Requires mcrypt functions.
|
||||
Warning: changing this key will make your stored feed passwords impossible
|
||||
to decrypt.
|
||||
'';
|
||||
};
|
||||
|
||||
singleUserMode = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
|
||||
description = ''
|
||||
Operate in single user mode, disables all functionality related to
|
||||
multiple users and authentication. Enabling this assumes you have
|
||||
your tt-rss directory protected by other means (e.g. http auth).
|
||||
'';
|
||||
};
|
||||
|
||||
simpleUpdateMode = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enables fallback update mode where tt-rss tries to update feeds in
|
||||
background while tt-rss is open in your browser.
|
||||
If you don't have a lot of feeds and don't want to or can't run
|
||||
background processes while not running tt-rss, this method is generally
|
||||
viable to keep your feeds up to date.
|
||||
Still, there are more robust (and recommended) updating methods
|
||||
available, you can read about them here: http://tt-rss.org/wiki/UpdatingFeeds
|
||||
'';
|
||||
};
|
||||
|
||||
forceArticlePurge = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = ''
|
||||
When this option is not 0, users ability to control feed purging
|
||||
intervals is disabled and all articles (which are not starred)
|
||||
older than this amount of days are purged.
|
||||
'';
|
||||
};
|
||||
|
||||
enableGZipOutput = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Selectively gzip output to improve wire performance. This requires
|
||||
PHP Zlib extension on the server.
|
||||
Enabling this can break tt-rss in several httpd/php configurations,
|
||||
if you experience weird errors and tt-rss failing to start, blank pages
|
||||
after login, or content encoding errors, disable it.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = ["auth_internal" "note"];
|
||||
description = ''
|
||||
List of plugins to load automatically for all users.
|
||||
System plugins have to be specified here. Please enable at least one
|
||||
authentication plugin here (auth_*).
|
||||
Users may enable other user plugins from Preferences/Plugins but may not
|
||||
disable plugins specified in this list.
|
||||
Disabling auth_internal in this list would automatically disable
|
||||
reset password link on the login form.
|
||||
'';
|
||||
};
|
||||
|
||||
pluginPackages = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
description = ''
|
||||
List of plugins to install. The list elements are expected to
|
||||
be derivations. All elements in this derivation are automatically
|
||||
copied to the <literal>plugins.local</literal> directory.
|
||||
'';
|
||||
};
|
||||
|
||||
themePackages = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
description = ''
|
||||
List of themes to install. The list elements are expected to
|
||||
be derivations. All elements in this derivation are automatically
|
||||
copied to the <literal>themes.local</literal> directory.
|
||||
'';
|
||||
};
|
||||
|
||||
logDestination = mkOption {
|
||||
type = types.enum ["" "sql" "syslog"];
|
||||
default = "sql";
|
||||
description = ''
|
||||
Log destination to use. Possible values: sql (uses internal logging
|
||||
you can read in Preferences -> System), syslog - logs to system log.
|
||||
Setting this to blank uses PHP logging (usually to http server
|
||||
error.log).
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Additional lines to append to <literal>config.php</literal>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
(mkRemovedOptionModule ["services" "tt-rss" "checkForUpdates"] ''
|
||||
This option was removed because setting this to true will cause TT-RSS
|
||||
to be unable to start if an automatic update of the code in
|
||||
services.tt-rss.root leads to a database schema upgrade that is not
|
||||
supported by the code active in the Nix store.
|
||||
'')
|
||||
];
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.database.password != null -> cfg.database.passwordFile == null;
|
||||
message = "Cannot set both password and passwordFile";
|
||||
}
|
||||
];
|
||||
|
||||
services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
|
||||
${poolName} = {
|
||||
inherit (cfg) user;
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0600";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 10;
|
||||
"pm.min_spare_servers" = 5;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# NOTE: No configuration is done if not using virtual host
|
||||
services.nginx = mkIf (cfg.virtualHost != null) {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
${cfg.virtualHost} = {
|
||||
root = "${cfg.root}/www";
|
||||
|
||||
locations."/" = {
|
||||
index = "index.php";
|
||||
};
|
||||
|
||||
locations."^~ /feed-icons" = {
|
||||
root = "${cfg.root}";
|
||||
};
|
||||
|
||||
locations."~ \\.php$" = {
|
||||
extraConfig = ''
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
|
||||
fastcgi_index index.php;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.root}' 0555 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/lock' 0755 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/cache' 0755 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/cache/upload' 0755 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/cache/images' 0755 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/cache/export' 0755 ${cfg.user} tt_rss - -"
|
||||
"d '${cfg.root}/feed-icons' 0755 ${cfg.user} tt_rss - -"
|
||||
"L+ '${cfg.root}/www' - - - - ${servedRoot}"
|
||||
];
|
||||
|
||||
systemd.services = {
|
||||
phpfpm-tt-rss = mkIf (cfg.pool == "${poolName}") {
|
||||
restartTriggers = [ servedRoot ];
|
||||
};
|
||||
|
||||
tt-rss = {
|
||||
description = "Tiny Tiny RSS feeds update daemon";
|
||||
|
||||
preStart = let
|
||||
callSql = e:
|
||||
if cfg.database.type == "pgsql" then ''
|
||||
${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \
|
||||
${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \
|
||||
${config.services.postgresql.package}/bin/psql \
|
||||
-U ${cfg.database.user} \
|
||||
${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \
|
||||
-c '${e}' \
|
||||
${cfg.database.name}''
|
||||
|
||||
else if cfg.database.type == "mysql" then ''
|
||||
echo '${e}' | ${config.services.mysql.package}/bin/mysql \
|
||||
-u ${cfg.database.user} \
|
||||
${optionalString (cfg.database.password != null) "-p${cfg.database.password}"} \
|
||||
${optionalString (cfg.database.host != null) "-h ${cfg.database.host} -P ${toString dbPort}"} \
|
||||
${cfg.database.name}''
|
||||
|
||||
else "";
|
||||
|
||||
in (optionalString (cfg.database.type == "pgsql") ''
|
||||
exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \
|
||||
| tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//')
|
||||
|
||||
if [ "$exists" == 'f' ]; then
|
||||
${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"}
|
||||
else
|
||||
echo 'The database contains some data. Leaving it as it is.'
|
||||
fi;
|
||||
'')
|
||||
|
||||
+ (optionalString (cfg.database.type == "mysql") ''
|
||||
exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \
|
||||
| tail -n+2 | sed -e 's/[ \n\t]*//')
|
||||
|
||||
if [ "$exists" == '0' ]; then
|
||||
${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"}
|
||||
else
|
||||
echo 'The database contains some data. Leaving it as it is.'
|
||||
fi;
|
||||
'');
|
||||
|
||||
serviceConfig = {
|
||||
User = "${cfg.user}";
|
||||
Group = "tt_rss";
|
||||
ExecStart = "${pkgs.php}/bin/php ${cfg.root}/www/update.php --daemon --quiet";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "60";
|
||||
SyslogIdentifier = "tt-rss";
|
||||
};
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
after = [ "network.target" ] ++ optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
|
||||
};
|
||||
};
|
||||
|
||||
services.mysql = mkIf mysqlLocal {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.user;
|
||||
ensurePermissions = {
|
||||
"${cfg.database.name}.*" = "ALL PRIVILEGES";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.postgresql = mkIf pgsqlLocal {
|
||||
enable = mkDefault true;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [
|
||||
{ name = cfg.user;
|
||||
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
users.users.tt_rss = optionalAttrs (cfg.user == "tt_rss") {
|
||||
description = "tt-rss service user";
|
||||
isSystemUser = true;
|
||||
group = "tt_rss";
|
||||
};
|
||||
|
||||
users.groups.tt_rss = {};
|
||||
};
|
||||
}
|
||||
145
nixos/modules/services/web-apps/vikunja.nix
Normal file
145
nixos/modules/services/web-apps/vikunja.nix
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
{ pkgs, lib, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.vikunja;
|
||||
format = pkgs.formats.yaml {};
|
||||
configFile = format.generate "config.yaml" cfg.settings;
|
||||
useMysql = cfg.database.type == "mysql";
|
||||
usePostgresql = cfg.database.type == "postgres";
|
||||
in {
|
||||
options.services.vikunja = with lib; {
|
||||
enable = mkEnableOption "vikunja service";
|
||||
package-api = mkOption {
|
||||
default = pkgs.vikunja-api;
|
||||
type = types.package;
|
||||
defaultText = literalExpression "pkgs.vikunja-api";
|
||||
description = "vikunja-api derivation to use.";
|
||||
};
|
||||
package-frontend = mkOption {
|
||||
default = pkgs.vikunja-frontend;
|
||||
type = types.package;
|
||||
defaultText = literalExpression "pkgs.vikunja-frontend";
|
||||
description = "vikunja-frontend derivation to use.";
|
||||
};
|
||||
environmentFiles = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [ ];
|
||||
description = ''
|
||||
List of environment files set in the vikunja systemd service.
|
||||
For example passwords should be set in one of these files.
|
||||
'';
|
||||
};
|
||||
setupNginx = mkOption {
|
||||
type = types.bool;
|
||||
default = config.services.nginx.enable;
|
||||
defaultText = literalExpression "config.services.nginx.enable";
|
||||
description = ''
|
||||
Whether to setup NGINX.
|
||||
Further nginx configuration can be done by changing
|
||||
<option>services.nginx.virtualHosts.<frontendHostname></option>.
|
||||
This does not enable TLS or ACME by default. To enable this, set the
|
||||
<option>services.nginx.virtualHosts.<frontendHostname>.enableACME</option> to
|
||||
<literal>true</literal> and if appropriate do the same for
|
||||
<option>services.nginx.virtualHosts.<frontendHostname>.forceSSL</option>.
|
||||
'';
|
||||
};
|
||||
frontendScheme = mkOption {
|
||||
type = types.enum [ "http" "https" ];
|
||||
description = ''
|
||||
Whether the site is available via http or https.
|
||||
This does not configure https or ACME in nginx!
|
||||
'';
|
||||
};
|
||||
frontendHostname = mkOption {
|
||||
type = types.str;
|
||||
description = "The Hostname under which the frontend is running.";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = format.type;
|
||||
default = {};
|
||||
description = ''
|
||||
Vikunja configuration. Refer to
|
||||
<link xlink:href="https://vikunja.io/docs/config-options/"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
};
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "sqlite" "mysql" "postgres" ];
|
||||
example = "postgres";
|
||||
default = "sqlite";
|
||||
description = "Database engine to use.";
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address. Can also be a socket.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "vikunja";
|
||||
description = "Database user.";
|
||||
};
|
||||
database = mkOption {
|
||||
type = types.str;
|
||||
default = "vikunja";
|
||||
description = "Database name.";
|
||||
};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/vikunja/vikunja.db";
|
||||
description = "Path to the sqlite3 database file.";
|
||||
};
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.vikunja.settings = {
|
||||
database = {
|
||||
inherit (cfg.database) type host user database path;
|
||||
};
|
||||
service = {
|
||||
frontendurl = "${cfg.frontendScheme}://${cfg.frontendHostname}/";
|
||||
};
|
||||
files = {
|
||||
basepath = "/var/lib/vikunja/files";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.vikunja-api = {
|
||||
description = "vikunja-api";
|
||||
after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [ cfg.package-api ];
|
||||
restartTriggers = [ configFile ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
DynamicUser = true;
|
||||
StateDirectory = "vikunja";
|
||||
ExecStart = "${cfg.package-api}/bin/vikunja";
|
||||
Restart = "always";
|
||||
EnvironmentFile = cfg.environmentFiles;
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts."${cfg.frontendHostname}" = mkIf cfg.setupNginx {
|
||||
locations = {
|
||||
"/" = {
|
||||
root = cfg.package-frontend;
|
||||
tryFiles = "try_files $uri $uri/ /";
|
||||
};
|
||||
"~* ^/(api|dav|\\.well-known)/" = {
|
||||
proxyPass = "http://localhost:3456";
|
||||
extraConfig = ''
|
||||
client_max_body_size 20M;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.etc."vikunja/config.yaml".source = configFile;
|
||||
};
|
||||
}
|
||||
73
nixos/modules/services/web-apps/virtlyst.nix
Normal file
73
nixos/modules/services/web-apps/virtlyst.nix
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.virtlyst;
|
||||
stateDir = "/var/lib/virtlyst";
|
||||
|
||||
ini = pkgs.writeText "virtlyst-config.ini" ''
|
||||
[wsgi]
|
||||
master = true
|
||||
threads = auto
|
||||
http-socket = ${cfg.httpSocket}
|
||||
application = ${pkgs.virtlyst}/lib/libVirtlyst.so
|
||||
chdir2 = ${stateDir}
|
||||
static-map = /static=${pkgs.virtlyst}/root/static
|
||||
|
||||
[Cutelyst]
|
||||
production = true
|
||||
DatabasePath = virtlyst.sqlite
|
||||
TemplatePath = ${pkgs.virtlyst}/root/src
|
||||
|
||||
[Rules]
|
||||
cutelyst.* = true
|
||||
virtlyst.* = true
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options.services.virtlyst = {
|
||||
enable = mkEnableOption "Virtlyst libvirt web interface";
|
||||
|
||||
adminPassword = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Initial admin password with which the database will be seeded.
|
||||
'';
|
||||
};
|
||||
|
||||
httpSocket = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost:3000";
|
||||
description = ''
|
||||
IP and/or port to which to bind the http socket.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.virtlyst = {
|
||||
home = stateDir;
|
||||
createHome = true;
|
||||
group = mkIf config.virtualisation.libvirtd.enable "libvirtd";
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
systemd.services.virtlyst = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
environment = {
|
||||
VIRTLYST_ADMIN_PASSWORD = cfg.adminPassword;
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.cutelyst}/bin/cutelyst-wsgi2 --ini ${ini}";
|
||||
User = "virtlyst";
|
||||
WorkingDirectory = stateDir;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
52
nixos/modules/services/web-apps/whitebophir.nix
Normal file
52
nixos/modules/services/web-apps/whitebophir.nix
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.whitebophir;
|
||||
in {
|
||||
options = {
|
||||
services.whitebophir = {
|
||||
enable = mkEnableOption "whitebophir, an online collaborative whiteboard server (persistent state will be maintained under <filename>/var/lib/whitebophir</filename>)";
|
||||
|
||||
package = mkOption {
|
||||
default = pkgs.whitebophir;
|
||||
defaultText = literalExpression "pkgs.whitebophir";
|
||||
type = types.package;
|
||||
description = "Whitebophir package to use.";
|
||||
};
|
||||
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "0.0.0.0";
|
||||
description = "Address to listen on (use 0.0.0.0 to allow access from any address).";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 5001;
|
||||
description = "Port to bind to.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.whitebophir = {
|
||||
description = "Whitebophir Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
environment = {
|
||||
PORT = toString cfg.port;
|
||||
HOST = toString cfg.listenAddress;
|
||||
WBO_HISTORY_DIR = "/var/lib/whitebophir";
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
ExecStart = "${cfg.package}/bin/whitebophir";
|
||||
Restart = "always";
|
||||
StateDirectory = "whitebophir";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
139
nixos/modules/services/web-apps/wiki-js.nix
Normal file
139
nixos/modules/services/web-apps/wiki-js.nix
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.wiki-js;
|
||||
|
||||
format = pkgs.formats.json { };
|
||||
|
||||
configFile = format.generate "wiki-js.yml" cfg.settings;
|
||||
in {
|
||||
options.services.wiki-js = {
|
||||
enable = mkEnableOption "wiki-js";
|
||||
|
||||
environmentFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/root/wiki-js.env";
|
||||
description = ''
|
||||
Environment fiel to inject e.g. secrets into the configuration.
|
||||
'';
|
||||
};
|
||||
|
||||
stateDirectoryName = mkOption {
|
||||
default = "wiki-js";
|
||||
type = types.str;
|
||||
description = ''
|
||||
Name of the directory in <filename>/var/lib</filename>.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
default = {};
|
||||
type = types.submodule {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3000;
|
||||
description = ''
|
||||
TCP port the process should listen to.
|
||||
'';
|
||||
};
|
||||
|
||||
bindIP = mkOption {
|
||||
default = "0.0.0.0";
|
||||
type = types.str;
|
||||
description = ''
|
||||
IPs the service should listen to.
|
||||
'';
|
||||
};
|
||||
|
||||
db = {
|
||||
type = mkOption {
|
||||
default = "postgres";
|
||||
type = types.enum [ "postgres" "mysql" "mariadb" "mssql" ];
|
||||
description = ''
|
||||
Database driver to use for persistence. Please note that <literal>sqlite</literal>
|
||||
is currently not supported as the build process for it is currently not implemented
|
||||
in <package>pkgs.wiki-js</package> and it's not recommended by upstream for
|
||||
production use.
|
||||
'';
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
example = "/run/postgresql";
|
||||
description = ''
|
||||
Hostname or socket-path to connect to.
|
||||
'';
|
||||
};
|
||||
db = mkOption {
|
||||
default = "wiki";
|
||||
type = types.str;
|
||||
description = ''
|
||||
Name of the database to use.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
default = "info";
|
||||
type = types.enum [ "error" "warn" "info" "verbose" "debug" "silly" ];
|
||||
description = ''
|
||||
Define how much detail is supposed to be logged at runtime.
|
||||
'';
|
||||
};
|
||||
|
||||
offline = mkEnableOption "offline mode" // {
|
||||
description = ''
|
||||
Disable latest file updates and enable
|
||||
<link xlink:href="https://docs.requarks.io/install/sideload">sideloading</link>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
Settings to configure <package>wiki-js</package>. This directly
|
||||
corresponds to <link xlink:href="https://docs.requarks.io/install/config">the upstream
|
||||
configuration options</link>.
|
||||
|
||||
Secrets can be injected via the environment by
|
||||
<itemizedlist>
|
||||
<listitem><para>specifying <xref linkend="opt-services.wiki-js.environmentFile" />
|
||||
to contain secrets</para></listitem>
|
||||
<listitem><para>and setting sensitive values to <literal>$(ENVIRONMENT_VAR)</literal>
|
||||
with this value defined in the environment-file.</para></listitem>
|
||||
</itemizedlist>
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.wiki-js.settings.dataPath = "/var/lib/${cfg.stateDirectoryName}";
|
||||
systemd.services.wiki-js = {
|
||||
description = "A modern and powerful wiki app built on Node.js";
|
||||
documentation = [ "https://docs.requarks.io/" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = with pkgs; [ coreutils ];
|
||||
preStart = ''
|
||||
ln -sf ${configFile} /var/lib/${cfg.stateDirectoryName}/config.yml
|
||||
ln -sf ${pkgs.wiki-js}/server /var/lib/${cfg.stateDirectoryName}
|
||||
ln -sf ${pkgs.wiki-js}/assets /var/lib/${cfg.stateDirectoryName}
|
||||
ln -sf ${pkgs.wiki-js}/package.json /var/lib/${cfg.stateDirectoryName}/package.json
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
||||
StateDirectory = cfg.stateDirectoryName;
|
||||
WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
|
||||
DynamicUser = true;
|
||||
PrivateTmp = true;
|
||||
ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.wiki-js}/server";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with maintainers; [ ma27 ];
|
||||
}
|
||||
480
nixos/modules/services/web-apps/wordpress.nix
Normal file
480
nixos/modules/services/web-apps/wordpress.nix
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.wordpress;
|
||||
eachSite = cfg.sites;
|
||||
user = "wordpress";
|
||||
webserver = config.services.${cfg.webserver};
|
||||
stateDir = hostName: "/var/lib/wordpress/${hostName}";
|
||||
|
||||
pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
|
||||
pname = "wordpress-${hostName}";
|
||||
version = src.version;
|
||||
src = cfg.package;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r * $out/
|
||||
|
||||
# symlink the wordpress config
|
||||
ln -s ${wpConfig hostName cfg} $out/share/wordpress/wp-config.php
|
||||
# symlink uploads directory
|
||||
ln -s ${cfg.uploadsDir} $out/share/wordpress/wp-content/uploads
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/pull/53399
|
||||
#
|
||||
# Symlinking works for most plugins and themes, but Avada, for instance, fails to
|
||||
# understand the symlink, causing its file path stripping to fail. This results in
|
||||
# requests that look like: https://example.com/wp-content//nix/store/...plugin/path/some-file.js
|
||||
# Since hard linking directories is not allowed, copying is the next best thing.
|
||||
|
||||
# copy additional plugin(s) and theme(s)
|
||||
${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes}
|
||||
${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins}
|
||||
'';
|
||||
};
|
||||
|
||||
wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" ''
|
||||
<?php
|
||||
define('DB_NAME', '${cfg.database.name}');
|
||||
define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}');
|
||||
define('DB_USER', '${cfg.database.user}');
|
||||
${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"}
|
||||
define('DB_CHARSET', 'utf8');
|
||||
$table_prefix = '${cfg.database.tablePrefix}';
|
||||
|
||||
require_once('${stateDir hostName}/secret-keys.php');
|
||||
|
||||
# wordpress is installed onto a read-only file system
|
||||
define('DISALLOW_FILE_EDIT', true);
|
||||
define('AUTOMATIC_UPDATER_DISABLED', true);
|
||||
|
||||
${cfg.extraConfig}
|
||||
|
||||
if ( !defined('ABSPATH') )
|
||||
define('ABSPATH', dirname(__FILE__) . '/');
|
||||
|
||||
require_once(ABSPATH . 'wp-settings.php');
|
||||
?>
|
||||
'';
|
||||
|
||||
secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
|
||||
secretsScript = hostStateDir: ''
|
||||
# The match in this line is not a typo, see https://github.com/NixOS/nixpkgs/pull/124839
|
||||
grep -q "LOOGGED_IN_KEY" "${hostStateDir}/secret-keys.php" && rm "${hostStateDir}/secret-keys.php"
|
||||
if ! test -e "${hostStateDir}/secret-keys.php"; then
|
||||
umask 0177
|
||||
echo "<?php" >> "${hostStateDir}/secret-keys.php"
|
||||
${concatMapStringsSep "\n" (var: ''
|
||||
echo "define('${var}', '`tr -dc a-zA-Z0-9 </dev/urandom | head -c 64`');" >> "${hostStateDir}/secret-keys.php"
|
||||
'') secretsVars}
|
||||
echo "?>" >> "${hostStateDir}/secret-keys.php"
|
||||
chmod 440 "${hostStateDir}/secret-keys.php"
|
||||
fi
|
||||
'';
|
||||
|
||||
siteOpts = { lib, name, ... }:
|
||||
{
|
||||
options = {
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.wordpress;
|
||||
defaultText = literalExpression "pkgs.wordpress";
|
||||
description = "Which WordPress package to use.";
|
||||
};
|
||||
|
||||
uploadsDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/wordpress/${name}/uploads";
|
||||
description = ''
|
||||
This directory is used for uploads of pictures. The directory passed here is automatically
|
||||
created and permissions adjusted as required.
|
||||
'';
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
description = ''
|
||||
List of path(s) to respective plugin(s) which are copied from the 'plugins' directory.
|
||||
<note><para>These plugins need to be packaged before use, see example.</para></note>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
let
|
||||
# Wordpress plugin 'embed-pdf-viewer' installation example
|
||||
embedPdfViewerPlugin = pkgs.stdenv.mkDerivation {
|
||||
name = "embed-pdf-viewer-plugin";
|
||||
# Download the theme from the wordpress site
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip";
|
||||
sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
|
||||
};
|
||||
# We need unzip to build this package
|
||||
nativeBuildInputs = [ pkgs.unzip ];
|
||||
# Installing simply means copying all files to the output directory
|
||||
installPhase = "mkdir -p $out; cp -R * $out/";
|
||||
};
|
||||
# And then pass this theme to the themes list like this:
|
||||
in [ embedPdfViewerPlugin ]
|
||||
'';
|
||||
};
|
||||
|
||||
themes = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
description = ''
|
||||
List of path(s) to respective theme(s) which are copied from the 'theme' directory.
|
||||
<note><para>These themes need to be packaged before use, see example.</para></note>
|
||||
'';
|
||||
example = literalExpression ''
|
||||
let
|
||||
# Let's package the responsive theme
|
||||
responsiveTheme = pkgs.stdenv.mkDerivation {
|
||||
name = "responsive-theme";
|
||||
# Download the theme from the wordpress site
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://downloads.wordpress.org/theme/responsive.3.14.zip";
|
||||
sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
|
||||
};
|
||||
# We need unzip to build this package
|
||||
nativeBuildInputs = [ pkgs.unzip ];
|
||||
# Installing simply means copying all files to the output directory
|
||||
installPhase = "mkdir -p $out; cp -R * $out/";
|
||||
};
|
||||
# And then pass this theme to the themes list like this:
|
||||
in [ responsiveTheme ]
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "wordpress";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "wordpress";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/wordpress-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
tablePrefix = mkOption {
|
||||
type = types.str;
|
||||
default = "wp_";
|
||||
description = ''
|
||||
The $table_prefix is the value placed in the front of your database tables.
|
||||
Change the value if you want to use something other than wp_ for your database
|
||||
prefix. Typically this is changed if you are installing multiple WordPress blogs
|
||||
in the same database.
|
||||
|
||||
See <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php#table_prefix'/>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
defaultText = literalExpression "/run/mysqld/mysqld.sock";
|
||||
description = "Path to the unix socket file to use for authentication.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting <option>services.httpd.virtualHosts</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the WordPress PHP pool. See the documentation on <literal>php-fpm.conf</literal>
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Any additional text to be appended to the wp-config.php
|
||||
configuration file. This is a PHP script. For configuration
|
||||
settings, see <link xlink:href='https://codex.wordpress.org/Editing_wp-config.php'/>.
|
||||
'';
|
||||
example = ''
|
||||
define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config.virtualHost.hostName = mkDefault name;
|
||||
};
|
||||
in
|
||||
{
|
||||
# interface
|
||||
options = {
|
||||
services.wordpress = {
|
||||
|
||||
sites = mkOption {
|
||||
type = types.attrsOf (types.submodule siteOpts);
|
||||
default = {};
|
||||
description = "Specification of one or more WordPress sites to serve";
|
||||
};
|
||||
|
||||
webserver = mkOption {
|
||||
type = types.enum [ "httpd" "nginx" "caddy" ];
|
||||
default = "httpd";
|
||||
description = ''
|
||||
Whether to use apache2 or nginx for virtual host management.
|
||||
|
||||
Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
|
||||
|
||||
Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
config = mkIf (eachSite != {}) (mkMerge [{
|
||||
|
||||
assertions =
|
||||
(mapAttrsToList (hostName: cfg:
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
||||
message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
|
||||
}) eachSite) ++
|
||||
(mapAttrsToList (hostName: cfg:
|
||||
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
||||
message = ''services.wordpress.sites."${hostName}".database.passwordFile cannot be specified if services.wordpress.sites."${hostName}".database.createLocally is set to true.'';
|
||||
}) eachSite);
|
||||
|
||||
|
||||
services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = mapAttrsToList (hostName: cfg: cfg.database.name) eachSite;
|
||||
ensureUsers = mapAttrsToList (hostName: cfg:
|
||||
{ name = cfg.database.user;
|
||||
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
|
||||
}
|
||||
) eachSite;
|
||||
};
|
||||
|
||||
services.phpfpm.pools = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "wordpress-${hostName}" {
|
||||
inherit user;
|
||||
group = webserver.group;
|
||||
settings = {
|
||||
"listen.owner" = webserver.user;
|
||||
"listen.group" = webserver.group;
|
||||
} // cfg.poolConfig;
|
||||
}
|
||||
)) eachSite;
|
||||
|
||||
}
|
||||
|
||||
(mkIf (cfg.webserver == "httpd") {
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts = mapAttrs (hostName: cfg: mkMerge [ cfg.virtualHost {
|
||||
documentRoot = mkForce "${pkg hostName cfg}/share/wordpress";
|
||||
extraConfig = ''
|
||||
<Directory "${pkg hostName cfg}/share/wordpress">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
|
||||
# standard wordpress .htaccess contents
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]
|
||||
</IfModule>
|
||||
|
||||
DirectoryIndex index.php
|
||||
Require all granted
|
||||
Options +FollowSymLinks -Indexes
|
||||
</Directory>
|
||||
|
||||
# https://wordpress.org/support/article/hardening-wordpress/#securing-wp-config-php
|
||||
<Files wp-config.php>
|
||||
Require all denied
|
||||
</Files>
|
||||
'';
|
||||
} ]) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
{
|
||||
systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
|
||||
"d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
|
||||
"d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
|
||||
"Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
|
||||
]) eachSite);
|
||||
|
||||
systemd.services = mkMerge [
|
||||
(mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "wordpress-init-${hostName}" {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "phpfpm-wordpress-${hostName}.service" ];
|
||||
after = optional cfg.database.createLocally "mysql.service";
|
||||
script = secretsScript (stateDir hostName);
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
Group = webserver.group;
|
||||
};
|
||||
})) eachSite)
|
||||
|
||||
(optionalAttrs (any (v: v.database.createLocally) (attrValues eachSite)) {
|
||||
httpd.after = [ "mysql.service" ];
|
||||
})
|
||||
];
|
||||
|
||||
users.users.${user} = {
|
||||
group = webserver.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
}
|
||||
|
||||
(mkIf (cfg.webserver == "nginx") {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = mapAttrs (hostName: cfg: {
|
||||
serverName = mkDefault hostName;
|
||||
root = "${pkg hostName cfg}/share/wordpress";
|
||||
extraConfig = ''
|
||||
index index.php;
|
||||
'';
|
||||
locations = {
|
||||
"/" = {
|
||||
priority = 200;
|
||||
extraConfig = ''
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
'';
|
||||
};
|
||||
"~ \\.php$" = {
|
||||
priority = 500;
|
||||
extraConfig = ''
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket};
|
||||
fastcgi_index index.php;
|
||||
include "${config.services.nginx.package}/conf/fastcgi.conf";
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
# Mitigate https://httpoxy.org/ vulnerabilities
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
fastcgi_intercept_errors off;
|
||||
fastcgi_buffer_size 16k;
|
||||
fastcgi_buffers 4 16k;
|
||||
fastcgi_connect_timeout 300;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
'';
|
||||
};
|
||||
"~ /\\." = {
|
||||
priority = 800;
|
||||
extraConfig = "deny all;";
|
||||
};
|
||||
"~* /(?:uploads|files)/.*\\.php$" = {
|
||||
priority = 900;
|
||||
extraConfig = "deny all;";
|
||||
};
|
||||
"~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = {
|
||||
priority = 1000;
|
||||
extraConfig = ''
|
||||
expires max;
|
||||
log_not_found off;
|
||||
'';
|
||||
};
|
||||
};
|
||||
}) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
(mkIf (cfg.webserver == "caddy") {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
virtualHosts = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "http://${hostName}" {
|
||||
extraConfig = ''
|
||||
root * /${pkg hostName cfg}/share/wordpress
|
||||
file_server
|
||||
|
||||
php_fastcgi unix/${config.services.phpfpm.pools."wordpress-${hostName}".socket}
|
||||
|
||||
@uploads {
|
||||
path_regexp path /uploads\/(.*)\.php
|
||||
}
|
||||
rewrite @uploads /
|
||||
|
||||
@wp-admin {
|
||||
path not ^\/wp-admin/*
|
||||
}
|
||||
rewrite @wp-admin {path}/index.php?{query}
|
||||
'';
|
||||
}
|
||||
)) eachSite;
|
||||
};
|
||||
})
|
||||
|
||||
|
||||
]);
|
||||
}
|
||||
181
nixos/modules/services/web-apps/youtrack.nix
Normal file
181
nixos/modules/services/web-apps/youtrack.nix
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.youtrack;
|
||||
|
||||
extraAttr = concatStringsSep " " (mapAttrsToList (k: v: "-D${k}=${v}") (stdParams // cfg.extraParams));
|
||||
mergeAttrList = lib.foldl' lib.mergeAttrs {};
|
||||
|
||||
stdParams = mergeAttrList [
|
||||
(optionalAttrs (cfg.baseUrl != null) {
|
||||
"jetbrains.youtrack.baseUrl" = cfg.baseUrl;
|
||||
})
|
||||
{
|
||||
"java.aws.headless" = "true";
|
||||
"jetbrains.youtrack.disableBrowser" = "true";
|
||||
}
|
||||
];
|
||||
in
|
||||
{
|
||||
options.services.youtrack = {
|
||||
|
||||
enable = mkEnableOption "YouTrack service";
|
||||
|
||||
address = mkOption {
|
||||
description = ''
|
||||
The interface youtrack will listen on.
|
||||
'';
|
||||
default = "127.0.0.1";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
baseUrl = mkOption {
|
||||
description = ''
|
||||
Base URL for youtrack. Will be auto-detected and stored in database.
|
||||
'';
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
extraParams = mkOption {
|
||||
default = {};
|
||||
description = ''
|
||||
Extra parameters to pass to youtrack. See
|
||||
https://www.jetbrains.com/help/youtrack/standalone/YouTrack-Java-Start-Parameters.html
|
||||
for more information.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
"jetbrains.youtrack.overrideRootPassword" = "tortuga";
|
||||
}
|
||||
'';
|
||||
type = types.attrsOf types.str;
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
description = ''
|
||||
Package to use.
|
||||
'';
|
||||
type = types.package;
|
||||
default = pkgs.youtrack;
|
||||
defaultText = literalExpression "pkgs.youtrack";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
description = ''
|
||||
The port youtrack will listen on.
|
||||
'';
|
||||
default = 8080;
|
||||
type = types.int;
|
||||
};
|
||||
|
||||
statePath = mkOption {
|
||||
description = ''
|
||||
Where to keep the youtrack database.
|
||||
'';
|
||||
type = types.path;
|
||||
default = "/var/lib/youtrack";
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
description = ''
|
||||
Name of the nginx virtual host to use and setup.
|
||||
If null, do not setup anything.
|
||||
'';
|
||||
default = null;
|
||||
type = types.nullOr types.str;
|
||||
};
|
||||
|
||||
jvmOpts = mkOption {
|
||||
description = ''
|
||||
Extra options to pass to the JVM.
|
||||
See https://www.jetbrains.com/help/youtrack/standalone/Configure-JVM-Options.html
|
||||
for more information.
|
||||
'';
|
||||
type = types.separatedString " ";
|
||||
example = "-XX:MetaspaceSize=250m";
|
||||
default = "";
|
||||
};
|
||||
|
||||
maxMemory = mkOption {
|
||||
description = ''
|
||||
Maximum Java heap size
|
||||
'';
|
||||
type = types.str;
|
||||
default = "1g";
|
||||
};
|
||||
|
||||
maxMetaspaceSize = mkOption {
|
||||
description = ''
|
||||
Maximum java Metaspace memory.
|
||||
'';
|
||||
type = types.str;
|
||||
default = "350m";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
systemd.services.youtrack = {
|
||||
environment.HOME = cfg.statePath;
|
||||
environment.YOUTRACK_JVM_OPTS = "${extraAttr}";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = with pkgs; [ unixtools.hostname ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = "youtrack";
|
||||
Group = "youtrack";
|
||||
Restart = "on-failure";
|
||||
ExecStart = ''${cfg.package}/bin/youtrack --J-Xmx${cfg.maxMemory} --J-XX:MaxMetaspaceSize=${cfg.maxMetaspaceSize} ${cfg.jvmOpts} ${cfg.address}:${toString cfg.port}'';
|
||||
};
|
||||
};
|
||||
|
||||
users.users.youtrack = {
|
||||
description = "Youtrack service user";
|
||||
isSystemUser = true;
|
||||
home = cfg.statePath;
|
||||
createHome = true;
|
||||
group = "youtrack";
|
||||
};
|
||||
|
||||
users.groups.youtrack = {};
|
||||
|
||||
services.nginx = mkIf (cfg.virtualHost != null) {
|
||||
upstreams.youtrack.servers."${cfg.address}:${toString cfg.port}" = {};
|
||||
virtualHosts.${cfg.virtualHost}.locations = {
|
||||
"/" = {
|
||||
proxyPass = "http://youtrack";
|
||||
extraConfig = ''
|
||||
client_max_body_size 10m;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
'';
|
||||
};
|
||||
|
||||
"/api/eventSourceBus" = {
|
||||
proxyPass = "http://youtrack";
|
||||
extraConfig = ''
|
||||
proxy_cache off;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_set_header Connection "";
|
||||
chunked_transfer_encoding off;
|
||||
client_max_body_size 10m;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
238
nixos/modules/services/web-apps/zabbix.nix
Normal file
238
nixos/modules/services/web-apps/zabbix.nix
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
{ config, lib, options, pkgs, ... }:
|
||||
|
||||
let
|
||||
|
||||
inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
|
||||
inherit (lib) literalExpression mapAttrs optionalString versionAtLeast;
|
||||
|
||||
cfg = config.services.zabbixWeb;
|
||||
opt = options.services.zabbixWeb;
|
||||
fpm = config.services.phpfpm.pools.zabbix;
|
||||
|
||||
user = "zabbix";
|
||||
group = "zabbix";
|
||||
stateDir = "/var/lib/zabbix";
|
||||
|
||||
zabbixConfig = pkgs.writeText "zabbix.conf.php" ''
|
||||
<?php
|
||||
// Zabbix GUI configuration file.
|
||||
global $DB;
|
||||
$DB['TYPE'] = '${ { mysql = "MYSQL"; pgsql = "POSTGRESQL"; oracle = "ORACLE"; }.${cfg.database.type} }';
|
||||
$DB['SERVER'] = '${cfg.database.host}';
|
||||
$DB['PORT'] = '${toString cfg.database.port}';
|
||||
$DB['DATABASE'] = '${cfg.database.name}';
|
||||
$DB['USER'] = '${cfg.database.user}';
|
||||
# NOTE: file_get_contents adds newline at the end of returned string
|
||||
$DB['PASSWORD'] = ${if cfg.database.passwordFile != null then "trim(file_get_contents('${cfg.database.passwordFile}'), \"\\r\\n\")" else "''"};
|
||||
// Schema name. Used for IBM DB2 and PostgreSQL.
|
||||
$DB['SCHEMA'] = ''';
|
||||
$ZBX_SERVER = '${cfg.server.address}';
|
||||
$ZBX_SERVER_PORT = '${toString cfg.server.port}';
|
||||
$ZBX_SERVER_NAME = ''';
|
||||
$IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
in
|
||||
{
|
||||
# interface
|
||||
|
||||
options.services = {
|
||||
zabbixWeb = {
|
||||
enable = mkEnableOption "the Zabbix web interface";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.zabbix.web;
|
||||
defaultText = literalExpression "zabbix.web";
|
||||
description = "Which Zabbix package to use.";
|
||||
};
|
||||
|
||||
server = {
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
description = "The port of the Zabbix server to connect to.";
|
||||
default = 10051;
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
description = "The IP address or hostname of the Zabbix server to connect to.";
|
||||
default = "localhost";
|
||||
};
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "mysql" "pgsql" "oracle" ];
|
||||
example = "mysql";
|
||||
default = "pgsql";
|
||||
description = "Database engine to use.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Database host address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
default =
|
||||
if cfg.database.type == "mysql" then config.services.mysql.port
|
||||
else if cfg.database.type == "pgsql" then config.services.postgresql.port
|
||||
else 1521;
|
||||
defaultText = literalExpression ''
|
||||
if config.${opt.database.type} == "mysql" then config.${options.services.mysql.port}
|
||||
else if config.${opt.database.type} == "pgsql" then config.${options.services.postgresql.port}
|
||||
else 1521
|
||||
'';
|
||||
description = "Database host port.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "zabbix";
|
||||
description = "Database name.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "zabbix";
|
||||
description = "Database user.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/keys/zabbix-dbpassword";
|
||||
description = ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/run/postgresql";
|
||||
description = "Path to the unix socket file to use for authentication.";
|
||||
};
|
||||
};
|
||||
|
||||
virtualHost = mkOption {
|
||||
type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
|
||||
example = literalExpression ''
|
||||
{
|
||||
hostName = "zabbix.example.org";
|
||||
adminAddr = "webmaster@example.org";
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Apache configuration can be done by adapting <literal>services.httpd.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 32;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 2;
|
||||
"pm.max_spare_servers" = 4;
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the Zabbix PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Additional configuration to be copied verbatim into <filename>zabbix.conf.php</filename>.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
# implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.zabbixWeb.extraConfig = optionalString ((versionAtLeast config.system.stateVersion "20.09") && (versionAtLeast cfg.package.version "5.0.0")) ''
|
||||
$DB['DOUBLE_IEEE754'] = 'true';
|
||||
'';
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${stateDir}' 0750 ${user} ${group} - -"
|
||||
"d '${stateDir}/session' 0750 ${user} ${config.services.httpd.group} - -"
|
||||
];
|
||||
|
||||
services.phpfpm.pools.zabbix = {
|
||||
inherit user;
|
||||
group = config.services.httpd.group;
|
||||
phpOptions = ''
|
||||
# https://www.zabbix.com/documentation/current/manual/installation/install
|
||||
memory_limit = 128M
|
||||
post_max_size = 16M
|
||||
upload_max_filesize = 2M
|
||||
max_execution_time = 300
|
||||
max_input_time = 300
|
||||
session.auto_start = 0
|
||||
mbstring.func_overload = 0
|
||||
always_populate_raw_post_data = -1
|
||||
# https://bbs.archlinux.org/viewtopic.php?pid=1745214#p1745214
|
||||
session.save_path = ${stateDir}/session
|
||||
'' + optionalString (config.time.timeZone != null) ''
|
||||
date.timezone = "${config.time.timeZone}"
|
||||
'' + optionalString (cfg.database.type == "oracle") ''
|
||||
extension=${pkgs.phpPackages.oci8}/lib/php/extensions/oci8.so
|
||||
'';
|
||||
phpEnv.ZABBIX_CONFIG = "${zabbixConfig}";
|
||||
settings = {
|
||||
"listen.owner" = config.services.httpd.user;
|
||||
"listen.group" = config.services.httpd.group;
|
||||
} // cfg.poolConfig;
|
||||
};
|
||||
|
||||
services.httpd = {
|
||||
enable = true;
|
||||
adminAddr = mkDefault cfg.virtualHost.adminAddr;
|
||||
extraModules = [ "proxy_fcgi" ];
|
||||
virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
|
||||
documentRoot = mkForce "${cfg.package}/share/zabbix";
|
||||
extraConfig = ''
|
||||
<Directory "${cfg.package}/share/zabbix">
|
||||
<FilesMatch "\.php$">
|
||||
<If "-f %{REQUEST_FILENAME}">
|
||||
SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
|
||||
</If>
|
||||
</FilesMatch>
|
||||
AllowOverride all
|
||||
Options -Indexes
|
||||
DirectoryIndex index.php
|
||||
</Directory>
|
||||
'';
|
||||
} ];
|
||||
};
|
||||
|
||||
users.users.${user} = mapAttrs (name: mkDefault) {
|
||||
description = "Zabbix daemon user";
|
||||
uid = config.ids.uids.zabbix;
|
||||
inherit group;
|
||||
};
|
||||
|
||||
users.groups.${group} = mapAttrs (name: mkDefault) {
|
||||
gid = config.ids.gids.zabbix;
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue