freescout: init at 1.8.225 (#535307)
This commit is contained in:
@@ -22,6 +22,8 @@
|
||||
|
||||
- [Stump](https://www.stumpapp.dev/), a free and open source comics, manga and digital book server with OPDS support. Available as [services.stump](#opt-services.stump.enable).
|
||||
|
||||
- [Freescout](https://freescout.net/), a free, open source Helpdesk and shared mailbox. Available as [services.freescout](#opt-services.freescout.enable).
|
||||
|
||||
- [FlapAlerted](https://github.com/Kioubit/FlapAlerted), detects BGP flapping events and provides statistics based on BGP update messages. Available as [services.flap-alerted](#opt-services.flap-alerted.enable).
|
||||
|
||||
- [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable).
|
||||
|
||||
@@ -1664,6 +1664,7 @@
|
||||
./services/web-apps/firefly-iii.nix
|
||||
./services/web-apps/flarum.nix
|
||||
./services/web-apps/fluidd.nix
|
||||
./services/web-apps/freescout.nix
|
||||
./services/web-apps/freshrss.nix
|
||||
./services/web-apps/froide-govplan.nix
|
||||
./services/web-apps/galene.nix
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
# Simple alias variables
|
||||
user = "freescout";
|
||||
group = user;
|
||||
cfg = config.services.freescout;
|
||||
datadir = "/var/lib/freescout";
|
||||
cachedir = "/var/cache/freescout";
|
||||
fpmService = "phpfpm-${user}";
|
||||
|
||||
# Generated config and more complex templates / default variables
|
||||
autoDb = if !cfg.databaseSetup.enable then false else cfg.databaseSetup.kind;
|
||||
dbService = lib.optional (autoDb != false) (
|
||||
if autoDb == "mysql" then "mysql.service" else "postgresql.service"
|
||||
);
|
||||
db_config = lib.optionalAttrs (autoDb != false) (
|
||||
if autoDb == "mysql" then
|
||||
{
|
||||
DB_CONNECTION = "mysql";
|
||||
DB_HOST = "";
|
||||
DB_SOCKET = "/run/mysqld/mysqld.sock";
|
||||
DB_USERNAME = user;
|
||||
DB_DATABASE = user;
|
||||
}
|
||||
else
|
||||
{
|
||||
DB_CONNECTION = "pgsql";
|
||||
DB_HOST = "/run/postgresql";
|
||||
DB_DATABASE = user;
|
||||
DB_USERNAME = user;
|
||||
}
|
||||
);
|
||||
|
||||
raw_config = {
|
||||
APP_ENV = "production";
|
||||
APP_FORCE_HTTPS = true;
|
||||
APP_URL = "https://${cfg.domain}";
|
||||
APP_TIMEZONE = config.time.timeZone;
|
||||
APP_DISABLE_UPDATING = true;
|
||||
}
|
||||
// cfg.settings
|
||||
// db_config;
|
||||
app_config = dropNull raw_config;
|
||||
baseService = {
|
||||
path = [
|
||||
pkgs.ps
|
||||
artisanWrapped
|
||||
];
|
||||
requires = [
|
||||
# Using requires (instead of wants) since a failing config
|
||||
# is indeed critical and should not allow this service to continue
|
||||
"freescout-setup.service"
|
||||
]
|
||||
++ dbService;
|
||||
serviceConfig = {
|
||||
User = user;
|
||||
Group = group;
|
||||
};
|
||||
};
|
||||
|
||||
# Custom built packages / files / scripts
|
||||
phpPackage = cfg.phpPackage.buildEnv {
|
||||
# As of php8.5 opcache is required and automatically compiled in and thus is not available in
|
||||
# all anymore. To keep compatibility with older versions, still add if available.
|
||||
extensions =
|
||||
{ all, enabled }: enabled ++ [ all.iconv ] ++ (lib.optional (all ? opcache) all.opcache);
|
||||
# Don't log anything because we are not sure, if this may leak secrets
|
||||
# Logging can be increased, if we have time to check the logging library
|
||||
extraConfig = ''
|
||||
error_reporting = 0
|
||||
'';
|
||||
};
|
||||
|
||||
package = cfg.package.overrideAttrs (prev: {
|
||||
pname = "${prev.pname}-${cfg.domain}";
|
||||
postInstall = prev.postInstall or "" + ''
|
||||
ln -s ${datadir} $out/share/freescout/data
|
||||
'';
|
||||
});
|
||||
|
||||
artisanWrapped = pkgs.writeShellApplication {
|
||||
name = "artisan-wrapped";
|
||||
runtimeInputs = with pkgs; [
|
||||
util-linux
|
||||
];
|
||||
text = ''
|
||||
cd ${datadir}
|
||||
_runuser='exec'
|
||||
if [[ "$USER" != ${user} ]]; then
|
||||
_runuser='exec runuser --user ${user}'
|
||||
fi
|
||||
''${_runuser} ${lib.getExe phpPackage} ${package}/share/freescout/artisan "$@"
|
||||
'';
|
||||
};
|
||||
configFile = mkEnvFile "freescout.env" app_config;
|
||||
allSecrets = lib.catAttrs "_secret" (lib.collect isSecret app_config);
|
||||
|
||||
configSetupScript = pkgs.writeShellScript "freescout-config-setup" ''
|
||||
set -o errexit -o pipefail -o nounset -o errtrace
|
||||
shopt -s inherit_errexit
|
||||
PATH=${lib.makeBinPath [ pkgs.replace-secret ]}:$PATH
|
||||
cp ${configFile} "/tmp/raw.env";
|
||||
${mkSecretsReplacement "/tmp/raw.env" allSecrets}
|
||||
install -T --mode 400 -o ${user} -g ${group} "/tmp/raw.env" "${datadir}/.env"
|
||||
rm "/tmp/raw.env"
|
||||
'';
|
||||
|
||||
freescoutSetupScript =
|
||||
let
|
||||
rwPaths = [
|
||||
"storage/app"
|
||||
"storage/framework"
|
||||
"storage/framework/sessions"
|
||||
"storage/framework/views"
|
||||
"storage/framework/cache/data"
|
||||
"storage/logs"
|
||||
"bootstrap/cache"
|
||||
"public/css/builds"
|
||||
"public/js/builds"
|
||||
"Modules"
|
||||
"public/modules"
|
||||
];
|
||||
in
|
||||
''
|
||||
set -x
|
||||
umask 027
|
||||
# Working arround https://github.com/freescout-helpdesk/freescout/issues/2547
|
||||
# and having to manually clear cache when migrating from something around
|
||||
# ~1.8.159 (╯°□°)╯︵ ┻━┻
|
||||
# See: https://github.com/freescout-help-desk/freescout/issues/4366#issuecomment-2495993397
|
||||
rm -f ${datadir}/bootstrap/cache.php ${datadir}/bootstrap/cache/{config,packages,services}.php
|
||||
|
||||
ln -sf "${artisanWrapped}/bin/artisan-wrapped" "${datadir}/artisan"
|
||||
${lib.concatMapStringsSep "\n" (p: "mkdir -p ${datadir}/${p}") rwPaths}
|
||||
|
||||
# Migrate database and stuff
|
||||
# This does migrate, cache:clear, queue:restart
|
||||
${lib.getExe artisanWrapped} freescout:after-app-update
|
||||
'';
|
||||
|
||||
# Helper functions
|
||||
isSecret = v: lib.isAttrs v && v ? _secret && lib.strings.isConvertibleWithToString v._secret;
|
||||
hashSecret = p: builtins.hashString "sha256" (toString p);
|
||||
dropNull = lib.filterAttrsRecursive (
|
||||
_: v:
|
||||
!lib.elem v [
|
||||
null
|
||||
[ ]
|
||||
{ }
|
||||
]
|
||||
);
|
||||
mkEnvVars = lib.generators.toKeyValue {
|
||||
mkKeyValue =
|
||||
k: v:
|
||||
let
|
||||
value =
|
||||
with builtins;
|
||||
if isInt v then
|
||||
toString v
|
||||
else if isString v then
|
||||
v
|
||||
else if isBool v then
|
||||
lib.boolToString v
|
||||
else if isSecret v then
|
||||
hashSecret v._secret
|
||||
else
|
||||
throw "freescout: ${k} has unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}";
|
||||
in
|
||||
"${k}=${value}";
|
||||
};
|
||||
mkEnvFile = fname: values: pkgs.writeText fname (mkEnvVars values);
|
||||
mkSecretsReplacement =
|
||||
filePath:
|
||||
lib.concatMapStringsSep "\n" (
|
||||
sp:
|
||||
"replace-secret ${
|
||||
lib.escapeShellArgs [
|
||||
(hashSecret sp)
|
||||
sp
|
||||
]
|
||||
} ${filePath}"
|
||||
);
|
||||
in
|
||||
{
|
||||
options.services.freescout = with lib; {
|
||||
enable = mkEnableOption "FreeScout helpdesk application";
|
||||
|
||||
package = mkPackageOption pkgs "freescout" { };
|
||||
|
||||
phpPackage = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.php;
|
||||
description = "The php package to use";
|
||||
defaultText = literalExpression "pkgs.php";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain the freescout installation will run under";
|
||||
example = "support.mydomain.net";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = with types; attrsOf anything;
|
||||
apply = mapAttrs' (
|
||||
k: v: {
|
||||
name = toUpper k;
|
||||
value = v;
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = ''
|
||||
Settings to be set in the `.env` file. See
|
||||
<https://github.com/freescout-help-desk/freescout/blob/master/.env.example>
|
||||
for reference on available environment variables.
|
||||
|
||||
Will be merged with the shown defaults.
|
||||
'';
|
||||
defaultText = lib.literalExpression ''
|
||||
{
|
||||
APP_ENV = "production";
|
||||
APP_FORCE_HTTPS = true;
|
||||
APP_URL = "https://''${config.services.freescout.domain}";
|
||||
APP_TIMEZONE = config.time.timeZone;
|
||||
APP_DISABLE_UPDATING = true;
|
||||
}
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
# NOTE: MUST be 256 bits (32 bytes) in length, the form of base64:<base64 encoded key> is recommended.
|
||||
# You can generate a valid one using `echo "base64:$(openssl rand -base64 32)"`
|
||||
APP_KEY_FILE = "/run/secret/freescout/app_key";
|
||||
DB_CONNECTION = "mysql";
|
||||
DB_HOST = "localhost";
|
||||
DB_PORT = 3306;
|
||||
DB_DATABASE = "freescout";
|
||||
DB_USERNAME = "freescout";
|
||||
DB_PASSWORD._secret = "/run/secret/freescout/db_pass";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
poolConfig = mkOption {
|
||||
type =
|
||||
with types;
|
||||
attrsOf (oneOf [
|
||||
str
|
||||
int
|
||||
bool
|
||||
]);
|
||||
default = {
|
||||
"pm" = "ondemand";
|
||||
"pm.max_children" = 32;
|
||||
"pm.process_idle_timeout" = "120s";
|
||||
"pm.max_requests" = 500;
|
||||
};
|
||||
description = ''
|
||||
Options for the freescout PHP pool. See the documentation on `php-fpm.conf`
|
||||
for details on configuration directives.
|
||||
'';
|
||||
};
|
||||
|
||||
databaseSetup = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
description = "Whether to enable automatic database setup and configuration";
|
||||
default = true;
|
||||
};
|
||||
|
||||
kind = mkOption {
|
||||
type = types.enum [
|
||||
"mysql"
|
||||
"pgsql"
|
||||
];
|
||||
default = "pgsql";
|
||||
example = "mysql";
|
||||
description = "Type of database to automatically set up";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = (app_config ? "APP_KEY" || app_config ? "APP_KEY_FILE");
|
||||
message = "`services.freescout.settings.APP_KEY_FILE` is required!";
|
||||
}
|
||||
];
|
||||
|
||||
warnings =
|
||||
lib.optional (app_config ? "APP_KEY" && lib.isString app_config.APP_KEY)
|
||||
"`services.freescout.settings.APP_KEY` will be stored in the world readable nix store. Use `APP_KEY._secret` or `APP_KEY_FILE` instead!";
|
||||
|
||||
users.users.${user} = {
|
||||
inherit group;
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = datadir;
|
||||
homeMode = "750";
|
||||
};
|
||||
users.users.${config.services.nginx.user}.extraGroups = [ group ];
|
||||
users.groups.${group} = { };
|
||||
|
||||
services.postgresql = lib.mkIf (autoDb == "pgsql") {
|
||||
enable = true;
|
||||
ensureUsers = [
|
||||
{
|
||||
name = user;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
ensureDatabases = [
|
||||
app_config.DB_DATABASE
|
||||
];
|
||||
};
|
||||
|
||||
services.mysql = lib.mkIf (autoDb == "mysql") {
|
||||
enable = true;
|
||||
package = lib.mkDefault pkgs.mariadb;
|
||||
ensureUsers = [
|
||||
{
|
||||
name = user;
|
||||
ensurePermissions = {
|
||||
"${app_config.DB_DATABASE}.*" = "ALL PRIVILEGES";
|
||||
};
|
||||
}
|
||||
];
|
||||
ensureDatabases = [
|
||||
app_config.DB_DATABASE
|
||||
];
|
||||
};
|
||||
|
||||
services.phpfpm.pools.${user} = {
|
||||
inherit phpPackage user group;
|
||||
|
||||
phpOptions = ''
|
||||
display_errors = On
|
||||
display_startup_errors = On
|
||||
'';
|
||||
settings = {
|
||||
"listen.owner" = user;
|
||||
"listen.group" = config.services.nginx.group;
|
||||
"catch_workers_output" = true;
|
||||
}
|
||||
// cfg.poolConfig;
|
||||
};
|
||||
|
||||
systemd.services.${fpmService} = {
|
||||
# Somehow the webinterface shows
|
||||
inherit (baseService) path;
|
||||
};
|
||||
|
||||
systemd.services.freescout-setup = lib.recursiveUpdate baseService {
|
||||
description = "Preparational tasks for freescout";
|
||||
requires = dbService;
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = dbService;
|
||||
script = freescoutSetupScript;
|
||||
serviceConfig = {
|
||||
PrivateTmp = true;
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStartPre = "+${configSetupScript}";
|
||||
};
|
||||
};
|
||||
|
||||
# This needs to be manually started again and again
|
||||
# Freescout has its own scheduler built in to ensure tasks run at the desired frequency
|
||||
# --no-interaction makes sure, that the queue worker is not executed.
|
||||
# This is needed, because otherweise the queue worker process would continue running
|
||||
# thus block further schedule invocations until the queue worker terminates.
|
||||
# See https://github.com/freescout-help-desk/freescout/blob/74fa4b7d4f8288f8d3fb1d343308d3289c4d72e2/app/Console/Kernel.php#L195-L267
|
||||
systemd.services."freescout-schedule-run" = baseService // {
|
||||
startAt = "minutely";
|
||||
script = "${lib.getExe artisanWrapped} schedule:run --no-interaction";
|
||||
};
|
||||
|
||||
# This is both long-running but also stops quite frequently.
|
||||
# Seeing job restart counts in the thousands here is normal.
|
||||
systemd.services."freescout-queue" = lib.recursiveUpdate baseService {
|
||||
# Copying the output to storage/logs because it makes
|
||||
# debugging connection issues easier for the user.
|
||||
script = ''
|
||||
${lib.getExe artisanWrapped} \
|
||||
queue:work \
|
||||
--queue emails,default \
|
||||
--sleep=5 \
|
||||
-vv \
|
||||
--tries=20 \
|
||||
| tee -a ${datadir}/storage/logs/queue-jobs.log
|
||||
'';
|
||||
serviceConfig = {
|
||||
RestartSec = "15s";
|
||||
RuntimeMaxSec = "1h";
|
||||
Restart = "always";
|
||||
};
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "freescout-setup.service" ] ++ dbService;
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.${cfg.domain} =
|
||||
let
|
||||
vhostCfg = config.services.nginx.virtualHosts.${cfg.domain};
|
||||
optSsl = lib.optionalString (vhostCfg.forceSSL || vhostCfg.onlySSL) "fastcgi_param HTTPS on;";
|
||||
in
|
||||
{
|
||||
root = lib.mkForce "${package}/share/freescout/public";
|
||||
|
||||
locations = {
|
||||
"/" = {
|
||||
index = "index.php";
|
||||
tryFiles = "$uri $uri/ /index.php$is_args$args";
|
||||
|
||||
extraConfig = ''
|
||||
# Defeats E-Mail open tracking or possibly "real" exploits
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'";
|
||||
'';
|
||||
};
|
||||
|
||||
"~ \\.php$" = {
|
||||
tryFiles = "$uri $uri/ =404";
|
||||
extraConfig = ''
|
||||
fastcgi_index index.php;
|
||||
include ${pkgs.nginx}/conf/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_pass unix:${config.services.phpfpm.pools.${user}.socket};
|
||||
${optSsl}
|
||||
|
||||
# Defeats E-Mail open tracking or possibly "real" exploits
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'";
|
||||
'';
|
||||
};
|
||||
|
||||
"~* ^/storage/attachment/" = {
|
||||
tryFiles = "$uri $uri/ /index.php?$query_string";
|
||||
extraConfig = ''
|
||||
expires 1M;
|
||||
access_log off;
|
||||
'';
|
||||
};
|
||||
|
||||
"~* ^/(?:css|js)/.*\\.(?:css|js)$".extraConfig = ''
|
||||
expires 2d;
|
||||
access_log off;
|
||||
add_header Cache-Control "public, must-revalidate";
|
||||
# Defeats E-Mail open tracking or possibly "real" exploits
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'";
|
||||
'';
|
||||
|
||||
"~* ^/(?:css|fonts|img|installer|js|modules)$".extraConfig = ''
|
||||
expires 1M;
|
||||
access_log off;
|
||||
add_header Cache-Control "public, must-revalidate";
|
||||
'';
|
||||
|
||||
"~ /\\.".extraConfig = ''
|
||||
deny all;
|
||||
'';
|
||||
"^~ /(css|js)/builds/".root = "${cachedir}/public/";
|
||||
"^~ /storage/app/attachment/" = {
|
||||
alias = "${datadir}/storage/app/attachment/";
|
||||
extraConfig = ''
|
||||
internal;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -630,6 +630,9 @@ in
|
||||
forgejoPackage = pkgs.forgejo-lts;
|
||||
};
|
||||
freenet = runTest ./freenet.nix;
|
||||
freescout = import ./freescout {
|
||||
inherit runTest;
|
||||
};
|
||||
freeswitch = runTest ./freeswitch.nix;
|
||||
freetube = discoverTests (import ./freetube.nix);
|
||||
freshrss = import ./freshrss { inherit runTest; };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{ runTest }:
|
||||
{
|
||||
integration = runTest ./integration.nix;
|
||||
upgrade = runTest ./upgrade.nix;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
# This tests runs freescout and performs the following tests:
|
||||
# - Create amin user via the CLI
|
||||
# - Create mailbox, configured for sending and receiving
|
||||
# - Test if receiving, sending and notifications work
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
mailDomain = "freemail.local";
|
||||
freescoutDomain = "freescout.local";
|
||||
sendInitial = pkgs.writeShellScriptBin "send-initial" ''
|
||||
exec ${pkgs.dovecot}/libexec/dovecot/deliver -d freescout <<MAIL
|
||||
From: root@localhost
|
||||
To: freescout@localhost
|
||||
Subject: Hello NixOS!
|
||||
Message-ID: initialtestmail-$(date +%s)@localhost
|
||||
|
||||
I am just a test E-Mail to see if freescout is (somewhat) working.
|
||||
MAIL
|
||||
'';
|
||||
|
||||
keyFile = pkgs.writeText "freescout-app-key" "base64:J8ZgK5LZkhVKpmZvjjA700sNL7+Y6aQTus8ZnUNNAaE=";
|
||||
|
||||
baseTestNode =
|
||||
{
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
virtualisation.memorySize = 1024;
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
sendInitial
|
||||
jq
|
||||
];
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
8025
|
||||
];
|
||||
networking.extraHosts = ''
|
||||
127.0.0.1 ${mailDomain} ${freescoutDomain}
|
||||
'';
|
||||
|
||||
services.mailhog = {
|
||||
enable = true;
|
||||
setSendmail = false;
|
||||
};
|
||||
|
||||
users.users.alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
|
||||
users.users.bob = {
|
||||
isNormalUser = true;
|
||||
description = "Bob Foobar";
|
||||
password = "foobar";
|
||||
};
|
||||
|
||||
# Taken from from the parsedmarc.nix test
|
||||
services.postfix.enable = true;
|
||||
services.dovecot2 = {
|
||||
enable = true;
|
||||
settings = {
|
||||
dovecot_config_version = "2.4.4";
|
||||
dovecot_storage_version = "2.4.4";
|
||||
mail_uid = "vmail";
|
||||
mail_gid = "vmail";
|
||||
protocols = [
|
||||
"imap"
|
||||
"lmtp"
|
||||
];
|
||||
mail_driver = "maildir";
|
||||
mail_home = "${config.services.postfix.settings.main.mail_spool_directory}/{user}";
|
||||
"passdb static" = {
|
||||
fields = {
|
||||
nopassword = true;
|
||||
allow_nets = "local,0.0.0.0/0,::/0";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
users.users.freescout = {
|
||||
password = "foobar2342";
|
||||
};
|
||||
|
||||
services.freescout = {
|
||||
enable = true;
|
||||
domain = freescoutDomain;
|
||||
settings = {
|
||||
APP_KEY._secret = toString keyFile;
|
||||
APP_FORCE_HTTPS = false;
|
||||
APP_URL = "http://${freescoutDomain}";
|
||||
APP_REMOTE_HOST_WHITE_LIST = "localhost,127.0.0.1,::1";
|
||||
APP_DEBUG = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkNode =
|
||||
dbType:
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
baseTestNode
|
||||
];
|
||||
services.freescout.databaseSetup = {
|
||||
enable = true;
|
||||
kind = dbType;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "freescout-integration";
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
e1mo
|
||||
];
|
||||
|
||||
nodes = {
|
||||
# This may lead to duplicate tests, but ensures that
|
||||
# it's always tested on the current default version
|
||||
# even if the tests are not updated
|
||||
freescout_pgsql = mkNode "pgsql";
|
||||
|
||||
# Same as the freescout_pgsql_default node
|
||||
freescout_mysql = mkNode "mysql";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
for machine in [freescout_pgsql]:
|
||||
machine.wait_for_unit("postgresql")
|
||||
|
||||
for machine in [freescout_mysql]:
|
||||
machine.wait_for_unit("mysql")
|
||||
|
||||
all=[
|
||||
freescout_pgsql,
|
||||
freescout_mysql
|
||||
]
|
||||
|
||||
for machine in all:
|
||||
machine.wait_for_unit("nginx")
|
||||
machine.wait_for_unit("dovecot")
|
||||
machine.wait_for_unit("mailhog")
|
||||
machine.wait_for_open_port(1025)
|
||||
machine.wait_for_open_port(8025)
|
||||
machine.wait_for_unit("freescout-setup")
|
||||
|
||||
with subtest("Login works"):
|
||||
machine.succeed("/var/lib/freescout/artisan freescout:create-user --role=admin --firstName=Xenia --lastName=TheFox --email xenia@${freescoutDomain} --no-interaction --password=foo | grep 'User created with id'")
|
||||
token=machine.succeed("curl -fsSL --cookie-jar cjar 'http://${freescoutDomain}/login' | grep -Po '(?<= name=\"_token\" value=\")(\\w+)(?=\")'").strip()
|
||||
data=f"email=xenia%40${freescoutDomain}&password=foo&_token={token}&remember=on"
|
||||
machine.succeed(f"curl -sSfX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/login' | grep 'Redirecting to'")
|
||||
machine.succeed("curl -fsSL --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}' | grep 'Dashboard'")
|
||||
# Enable all (most except following) notifications
|
||||
to_enable=list(range(1, 9))
|
||||
enable_data="&".join(map(lambda n: "subscriptions%5B1%5D%5B%5D=" + str(n), range(1,9)))
|
||||
data=f"_token={token}&{enable_data}"
|
||||
machine.succeed(f"curl -sSfX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/users/notifications/1'")
|
||||
|
||||
with subtest("Create and edit Mailbox"):
|
||||
data=f"email=freescout%40${mailDomain}&name=Test+Mailbox&_token={token}"
|
||||
machine.succeed(f"curl -sSfX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/mailbox/new'")
|
||||
machine.succeed("curl -fsSL --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}' | grep 'Test Mailbox'")
|
||||
machine.succeed("curl -sSf --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}/mailbox/1' | grep 'freescout@${mailDomain}'")
|
||||
data=f"out_method=3&out_server=localhost&out_port=1025&out_username=&out_password=&out_encryption=1&_token={token}"
|
||||
machine.succeed(f"curl -sSfX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/mailbox/connection-settings/1/outgoing'")
|
||||
data=f"&in_protocol=1&in_server=localhost&in_port=143&in_username=freescout&in_password=super_secret&in_encryption=1&in_imap_folders%5B%5D=INBOX&imap_sent_folder=&_token={token}"
|
||||
machine.succeed(f"curl -fsSX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/mailbox/connection-settings/1/incoming'")
|
||||
data="&action=fetch_test&mailbox_id=1"
|
||||
machine.succeed(f"test $(curl -sSfX POST --cookie-jar cjar --cookie cjar -H 'X-CSRF-TOKEN: {token}' --data-raw '{data}' 'http://${freescoutDomain}/mailbox/ajax' | jq -r '.status') = 'success'")
|
||||
|
||||
with subtest("Send E-Mails"):
|
||||
machine.succeed("send-initial")
|
||||
|
||||
# Doing a second loop so that we won't have to wait that much
|
||||
for machine in all:
|
||||
with subtest("E-Mails ae received"):
|
||||
machine.wait_until_succeeds("curl -sSf --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}/mailbox/1' | grep 'Hello NixOS'", timeout=180)
|
||||
# Notifactions to users are being sent
|
||||
|
||||
for machine in all:
|
||||
with subtest("Notifications are sent"):
|
||||
machine.wait_until_succeeds("test $(curl -sSf http://127.0.0.1:8025/api/v2/messages | jq '.total') -eq 1", timeout=180)
|
||||
machine.succeed("curl -sSf http://127.0.0.1:8025/api/v2/messages | jq '.items[].Content.Headers[\"X-FreeScout-Mail-Type\"] | .[0]'")
|
||||
|
||||
with subtest("Ensure vars are being generated"):
|
||||
machine.succeed("curl -sSf 'http://${freescoutDomain}/'")
|
||||
machine.succeed("curl -sSf 'http://${freescoutDomain}/storage/js/vars.js'")
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# This tests checks, wether upgrading between versions works fine
|
||||
# In the past, there have been releases that would require manual deletion of specific
|
||||
# cache files, otherwise bricking the installation.
|
||||
# This test should catch similar instances in the future.
|
||||
# The oldFreescoutVersion may need a bump from time to time as there may be incompatibilities
|
||||
# with up-to-date databases on older freescout versions.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
freescoutDomain = "freescout.local";
|
||||
|
||||
oldFreescoutVersion = pkgs.freescout.overrideAttrs (oa: rec {
|
||||
version = "1.8.220";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "freescout-help-desk";
|
||||
repo = "freescout";
|
||||
tag = version;
|
||||
hash = "sha256-bOkazBcd9EKzQdZZA6YMn4+UNYhpDFV9hDMHR5kXke0=";
|
||||
};
|
||||
});
|
||||
newFreescoutVersion = pkgs.freescout;
|
||||
in
|
||||
{
|
||||
name = "freescout-upgrade";
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
e1mo
|
||||
];
|
||||
|
||||
nodes.machine =
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
networking.extraHosts = ''
|
||||
127.0.0.1 ${freescoutDomain}
|
||||
'';
|
||||
virtualisation.memorySize = 1024;
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
jq
|
||||
];
|
||||
services.freescout = {
|
||||
package = oldFreescoutVersion;
|
||||
enable = true;
|
||||
domain = freescoutDomain;
|
||||
settings = {
|
||||
APP_KEY = "base64:J8ZgK5LZkhVKpmZvjjA700sNL7+Y6aQTus8ZnUNNAaE=";
|
||||
APP_FORCE_HTTPS = false;
|
||||
APP_URL = "http://${freescoutDomain}:8888";
|
||||
APP_DEBUG = true;
|
||||
};
|
||||
databaseSetup = {
|
||||
enable = true;
|
||||
kind = "pgsql";
|
||||
};
|
||||
};
|
||||
specialisation.upgrade.configuration.services.freescout.package = lib.mkForce newFreescoutVersion;
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
oldVersion = nodes.machine.services.freescout.package.version;
|
||||
newVersion = nodes.machine.specialisation.upgrade.configuration.services.freescout.package.version;
|
||||
in
|
||||
''
|
||||
machine.start()
|
||||
machine.wait_for_unit("nginx")
|
||||
machine.wait_for_unit("postgresql")
|
||||
machine.wait_for_unit("freescout-setup")
|
||||
|
||||
with subtest("Create user and log in"):
|
||||
# Create uesr
|
||||
machine.succeed("/var/lib/freescout/artisan freescout:create-user --role=admin --firstName=Xenia --lastName=TheFox --email xenia@${freescoutDomain} --no-interaction --password=foo | grep 'User created with id'")
|
||||
# Obtain CSRF token
|
||||
token=machine.succeed("curl -fsSL --cookie-jar cjar 'http://${freescoutDomain}/login' | grep -Po '(?<= name=\"_token\" value=\")(\w+)(?=\")'").strip()
|
||||
# Actually log in
|
||||
data=f"email=xenia%40${freescoutDomain}&password=foo&_token={token}&remember=on"
|
||||
machine.succeed(f"curl -sSfX POST --cookie-jar cjar --cookie cjar --data-raw '{data}' 'http://${freescoutDomain}/login' | grep 'Redirecting to'")
|
||||
|
||||
with subtest("Check old API version"):
|
||||
machine.succeed("curl -fsSL --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}/system/status' | grep ${oldVersion}")
|
||||
|
||||
machine.execute("${nodes.machine.system.build.toplevel}/specialisation/upgrade/bin/switch-to-configuration test >&2")
|
||||
|
||||
machine.wait_for_unit("nginx")
|
||||
machine.wait_for_unit("freescout-setup")
|
||||
|
||||
with subtest("Check new API version"):
|
||||
machine.succeed("curl -fsSL --cookie-jar cjar --cookie cjar 'http://${freescoutDomain}/system/status' | grep ${newVersion}")
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
From 1c749a3610fb423e50260a2c807b44a94ef4c69d Mon Sep 17 00:00:00 2001
|
||||
From: Nina Fromm <git@e1mo.de>
|
||||
Date: Thu, 25 Jun 2026 16:23:00 +0200
|
||||
Subject: [PATCH] settings: catch unwritable .env
|
||||
|
||||
---
|
||||
app/Misc/Helper.php | 7 ++++++-
|
||||
1 file changed, 6 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/app/Misc/Helper.php b/app/Misc/Helper.php
|
||||
index 2d647b89..3fc2ef84 100644
|
||||
--- a/app/Misc/Helper.php
|
||||
+++ b/app/Misc/Helper.php
|
||||
@@ -1172,7 +1172,12 @@ class Helper
|
||||
$contents = $contents."\n{$key}={$value}\n";
|
||||
}
|
||||
}
|
||||
- \File::put($env_path, $contents);
|
||||
+ try {
|
||||
+ \File::put($env_path, $contents);
|
||||
+ } catch (\Exception $e) {
|
||||
+ \Helper::logException($e, 'Error updating ' . $key .' in .env file: ');
|
||||
+ \Session::flash('flash_error_unescaped', "Unable to write settings to <code>$env_path</code>: " . $e->getMessage());
|
||||
+ }
|
||||
}
|
||||
|
||||
/**
|
||||
--
|
||||
2.53.0
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
nixosTests,
|
||||
}:
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
preferLocalBuild = true;
|
||||
pname = "freescout";
|
||||
version = "1.8.225";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "freescout-help-desk";
|
||||
repo = "freescout";
|
||||
tag = finalAttrs.version;
|
||||
hash = "sha256-kXZ6bjF36YO1p6q+KTugnBO+KxqQZci5O0RNM7lqecQ=";
|
||||
};
|
||||
|
||||
patches = [
|
||||
./0001-settings-catch-unwritable-.env.patch
|
||||
];
|
||||
|
||||
prePatch = ''
|
||||
rm -rf storage
|
||||
rm bootstrap/cache/.gitignore
|
||||
rm public/{css,js}/builds/.htaccess
|
||||
rm {Modules,public/modules}/.gitkeep
|
||||
rmdir Modules public/modules bootstrap/cache public/{css,js}/builds
|
||||
ln -rs data/.env .env
|
||||
ln -rs data/storage storage
|
||||
ln -rs data/bootstrap/cache bootstrap/cache
|
||||
ln -rs data/storage/app/public public/storage
|
||||
ln -rs data/public/css/builds public/css/builds
|
||||
ln -rs data/public/js/builds public/js/builds
|
||||
ln -rs data/Modules Modules
|
||||
ln -rs data/public/modules public/modules
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/share/freescout
|
||||
cp -ar . $out/share/freescout
|
||||
chmod +x $out/share/freescout/artisan
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
passthru.tests = {
|
||||
inherit (nixosTests) freescout;
|
||||
};
|
||||
|
||||
# Because freescout is searching for some folders only relative to it's own source location, we need to have the symlinks to the actual locations in here
|
||||
dontCheckForBrokenSymlinks = true;
|
||||
strictDeps = true;
|
||||
__structuredAttrs = true;
|
||||
|
||||
meta = with lib; {
|
||||
description = "Free self-hosted help desk & shared mailbox";
|
||||
license = licenses.agpl3Only;
|
||||
homepage = "https://freescout.net/";
|
||||
platforms = platforms.all;
|
||||
maintainers = with maintainers; [ e1mo ];
|
||||
};
|
||||
})
|
||||
Reference in New Issue
Block a user