Merge pull request #224212 from Guekka/monica
monica: init at 4.0.0 & nixos-module
This commit is contained in:
commit
4f6b51d99c
|
@ -63,6 +63,8 @@ In addition to numerous new and upgraded packages, this release has the followin
|
|||
|
||||
- [opensearch](https://opensearch.org), a search server alternative to Elasticsearch. Available as [services.opensearch](options.html#opt-services.opensearch.enable).
|
||||
|
||||
- [monica](https://www.monicahq.com), an open source personal CRM. Available as [services.monica](options.html#opt-services.monica.enable).
|
||||
|
||||
- [authelia](https://www.authelia.com/), is an open-source authentication and authorization server. Available under [services.authelia](options.html#opt-services.authelia.enable).
|
||||
|
||||
- [goeland](https://github.com/slurdge/goeland), an alternative to rss2email written in golang with many filters. Available as [services.goeland](#opt-services.goeland.enable).
|
||||
|
|
|
@ -1183,6 +1183,7 @@
|
|||
./services/web-apps/mattermost.nix
|
||||
./services/web-apps/mediawiki.nix
|
||||
./services/web-apps/miniflux.nix
|
||||
./services/web-apps/monica.nix
|
||||
./services/web-apps/moodle.nix
|
||||
./services/web-apps/netbox.nix
|
||||
./services/web-apps/nextcloud.nix
|
||||
|
|
468
nixos/modules/services/web-apps/monica.nix
Normal file
468
nixos/modules/services/web-apps/monica.nix
Normal file
|
@ -0,0 +1,468 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.services.monica;
|
||||
monica = pkgs.monica.override {
|
||||
dataDir = cfg.dataDir;
|
||||
};
|
||||
db = cfg.database;
|
||||
mail = cfg.mail;
|
||||
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
|
||||
# shell script for local administration
|
||||
artisan = pkgs.writeScriptBin "monica" ''
|
||||
#! ${pkgs.runtimeShell}
|
||||
cd ${monica}
|
||||
sudo() {
|
||||
if [[ "$USER" != ${user} ]]; then
|
||||
exec /run/wrappers/bin/sudo -u ${user} "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
}
|
||||
sudo ${pkgs.php}/bin/php artisan "$@"
|
||||
'';
|
||||
|
||||
tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
|
||||
in {
|
||||
options.services.monica = {
|
||||
enable = mkEnableOption (lib.mdDoc "monica");
|
||||
|
||||
user = mkOption {
|
||||
default = "monica";
|
||||
description = lib.mdDoc "User monica runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
default = "monica";
|
||||
description = lib.mdDoc "Group monica runs as.";
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
appKeyFile = mkOption {
|
||||
description = lib.mdDoc ''
|
||||
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/monica-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 = "monica.example.com";
|
||||
description = lib.mdDoc ''
|
||||
The hostname to serve monica on.
|
||||
'';
|
||||
};
|
||||
|
||||
appURL = mkOption {
|
||||
description = lib.mdDoc ''
|
||||
The root URL that you want to host monica on. All URLs in monica 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 monica: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 = lib.mdDoc "monica data directory";
|
||||
default = "/var/lib/monica";
|
||||
type = types.path;
|
||||
};
|
||||
|
||||
database = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = lib.mdDoc "Database host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = lib.mdDoc "Database host port.";
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "monica";
|
||||
description = lib.mdDoc "Database name.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = user;
|
||||
defaultText = lib.literalExpression "user";
|
||||
description = lib.mdDoc "Database username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/monica-dbpassword";
|
||||
description = lib.mdDoc ''
|
||||
A file containing the password corresponding to
|
||||
<option>database.user</option>.
|
||||
'';
|
||||
};
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = lib.mdDoc "Create the database and database user locally.";
|
||||
};
|
||||
};
|
||||
|
||||
mail = {
|
||||
driver = mkOption {
|
||||
type = types.enum ["smtp" "sendmail"];
|
||||
default = "smtp";
|
||||
description = lib.mdDoc "Mail driver to use.";
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = lib.mdDoc "Mail host address.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 1025;
|
||||
description = lib.mdDoc "Mail host port.";
|
||||
};
|
||||
fromName = mkOption {
|
||||
type = types.str;
|
||||
default = "monica";
|
||||
description = lib.mdDoc "Mail \"from\" name.";
|
||||
};
|
||||
from = mkOption {
|
||||
type = types.str;
|
||||
default = "mail@monica.com";
|
||||
description = lib.mdDoc "Mail \"from\" email.";
|
||||
};
|
||||
user = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "monica";
|
||||
description = lib.mdDoc "Mail username.";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
example = "/run/keys/monica-mailpassword";
|
||||
description = lib.mdDoc ''
|
||||
A file containing the password corresponding to
|
||||
<option>mail.user</option>.
|
||||
'';
|
||||
};
|
||||
encryption = mkOption {
|
||||
type = with types; nullOr (enum ["tls"]);
|
||||
default = null;
|
||||
description = lib.mdDoc "SMTP encryption mechanism to use.";
|
||||
};
|
||||
};
|
||||
|
||||
maxUploadSize = mkOption {
|
||||
type = types.str;
|
||||
default = "18M";
|
||||
example = "1G";
|
||||
description = lib.mdDoc "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 = lib.mdDoc ''
|
||||
Options for the monica 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 = ''
|
||||
{
|
||||
serverAliases = [
|
||||
"monica.''${config.networking.domain}"
|
||||
];
|
||||
# To enable encryption and let let's encrypt take care of certificate
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
'';
|
||||
description = lib.mdDoc ''
|
||||
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 = lib.mdDoc ''
|
||||
The path to a file containing the value the
|
||||
option should be set to in the final
|
||||
configuration file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
})));
|
||||
default = {};
|
||||
example = ''
|
||||
{
|
||||
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 = "monica";
|
||||
OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
|
||||
OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
|
||||
OIDC_ISSUER_DISCOVER = true;
|
||||
}
|
||||
'';
|
||||
description = lib.mdDoc ''
|
||||
monica configuration options to set in the
|
||||
<filename>.env</filename> file.
|
||||
|
||||
Refer to <link xlink:href="https://github.com/monicahq/monica"/>
|
||||
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.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true.";
|
||||
}
|
||||
{
|
||||
assertion = db.createLocally -> db.passwordFile == null;
|
||||
message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true.";
|
||||
}
|
||||
];
|
||||
|
||||
services.monica.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;
|
||||
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/monica/cache/services.php";
|
||||
APP_PACKAGES_CACHE = "/run/monica/cache/packages.php";
|
||||
APP_CONFIG_CACHE = "/run/monica/cache/config.php";
|
||||
APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php";
|
||||
APP_EVENTS_CACHE = "/run/monica/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.monica = {
|
||||
inherit user 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;
|
||||
recommendedBrotliSettings = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts.${cfg.hostname} = mkMerge [
|
||||
cfg.nginx
|
||||
{
|
||||
root = mkForce "${monica}/public";
|
||||
locations = {
|
||||
"/" = {
|
||||
index = "index.php";
|
||||
tryFiles = "$uri $uri/ /index.php?$query_string";
|
||||
};
|
||||
"~ \.php$".extraConfig = ''
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket};
|
||||
'';
|
||||
"~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
|
||||
extraConfig = "expires 365d;";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.monica-setup = {
|
||||
description = "Preperation tasks for monica";
|
||||
before = ["phpfpm-monica.service"];
|
||||
after = optional db.createLocally "mysql.service";
|
||||
wantedBy = ["multi-user.target"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = user;
|
||||
UMask = 077;
|
||||
WorkingDirectory = "${monica}";
|
||||
RuntimeDirectory = "monica/cache";
|
||||
RuntimeDirectoryMode = 0700;
|
||||
};
|
||||
path = [pkgs.replace-secret];
|
||||
script = let
|
||||
isSecret = v: isAttrs v && v ? _secret && isString v._secret;
|
||||
monicaEnvVars = 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;
|
||||
monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig);
|
||||
in ''
|
||||
# error handling
|
||||
set -euo pipefail
|
||||
|
||||
# create .env file
|
||||
install -T -m 0600 -o ${user} ${monicaEnv} "${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 & seed db
|
||||
${pkgs.php}/bin/php artisan key:generate --force
|
||||
${pkgs.php}/bin/php artisan setup:production -v --force
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.monica-scheduler = {
|
||||
description = "Background tasks for monica";
|
||||
startAt = "minutely";
|
||||
after = ["monica-setup.service"];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = user;
|
||||
WorkingDirectory = "${monica}";
|
||||
ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v";
|
||||
};
|
||||
};
|
||||
|
||||
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 == "monica") {
|
||||
monica = {
|
||||
inherit group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
"${config.services.nginx.user}".extraGroups = [group];
|
||||
};
|
||||
groups = mkIf (group == "monica") {
|
||||
monica = {};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -421,6 +421,7 @@ in {
|
|||
mjolnir = handleTest ./matrix/mjolnir.nix {};
|
||||
mod_perl = handleTest ./mod_perl.nix {};
|
||||
molly-brown = handleTest ./molly-brown.nix {};
|
||||
monica = handleTest ./web-apps/monica.nix {};
|
||||
mongodb = handleTest ./mongodb.nix {};
|
||||
moodle = handleTest ./moodle.nix {};
|
||||
moonraker = handleTest ./moonraker.nix {};
|
||||
|
|
33
nixos/tests/web-apps/monica.nix
Normal file
33
nixos/tests/web-apps/monica.nix
Normal file
|
@ -0,0 +1,33 @@
|
|||
import ../make-test-python.nix ({pkgs, ...}:
|
||||
let
|
||||
cert = pkgs.runCommand "selfSignedCerts" { nativeBuildInputs = [ pkgs.openssl ]; } ''
|
||||
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=localhost' -days 36500
|
||||
mkdir -p $out
|
||||
cp key.pem cert.pem $out
|
||||
'';
|
||||
in
|
||||
{
|
||||
name = "monica";
|
||||
|
||||
nodes = {
|
||||
machine = {pkgs, ...}: {
|
||||
services.monica = {
|
||||
enable = true;
|
||||
hostname = "localhost";
|
||||
appKeyFile = "${pkgs.writeText "keyfile" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}";
|
||||
nginx = {
|
||||
forceSSL = true;
|
||||
sslCertificate = "${cert}/cert.pem";
|
||||
sslCertificateKey = "${cert}/key.pem";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.wait_for_unit("monica-setup.service")
|
||||
machine.wait_for_open_port(443)
|
||||
machine.succeed("curl -k --fail https://localhost", timeout=10)
|
||||
'';
|
||||
})
|
39
pkgs/servers/web-apps/monica/default.nix
Normal file
39
pkgs/servers/web-apps/monica/default.nix
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
stdenv,
|
||||
lib,
|
||||
fetchurl,
|
||||
nixosTests,
|
||||
dataDir ? "/var/lib/monica",
|
||||
}:
|
||||
stdenv.mkDerivation rec {
|
||||
pname = "monica";
|
||||
version = "4.0.0";
|
||||
|
||||
src = fetchurl {
|
||||
url = "https://github.com/monicahq/monica/releases/download/v${version}/monica-v${version}.tar.bz2";
|
||||
hash = "sha256-uHsRCO7P5w1JmKDwyLUVjK6NwnTF2mjsz0hOnPrms+w=";
|
||||
};
|
||||
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
cp -R * $out/
|
||||
rm -rf $out/storage
|
||||
ln -s ${dataDir}/.env $out/.env
|
||||
ln -s ${dataDir}/storage $out/storage
|
||||
'';
|
||||
|
||||
passthru.tests.monica = nixosTests.monica;
|
||||
|
||||
meta = {
|
||||
description = "Personal CRM";
|
||||
homepage = "https://www.monicahq.com/";
|
||||
longDescription = ''
|
||||
Remember everything about your friends, family and business
|
||||
relationships.
|
||||
'';
|
||||
license = lib.licenses.agpl3Plus;
|
||||
platforms = lib.platforms.all;
|
||||
};
|
||||
}
|
|
@ -1592,6 +1592,8 @@ with pkgs;
|
|||
buildGoModule = buildGo119Module; # go 1.20 build failure
|
||||
};
|
||||
|
||||
monica = callPackage ../servers/web-apps/monica { };
|
||||
|
||||
mprocs = callPackage ../tools/misc/mprocs { };
|
||||
|
||||
mpy-utils = python3Packages.callPackage ../tools/misc/mpy-utils { };
|
||||
|
|
Loading…
Reference in a new issue