freescout: init at 1.8.225 (#535307)

This commit is contained in:
Maximilian Bosch
2026-06-26 18:57:23 +00:00
committed by GitHub
9 changed files with 882 additions and 0 deletions
@@ -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).
+1
View File
@@ -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;
'';
};
};
};
};
};
}
+3
View File
@@ -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; };
+5
View File
@@ -0,0 +1,5 @@
{ runTest }:
{
integration = runTest ./integration.nix;
upgrade = runTest ./upgrade.nix;
}
+195
View File
@@ -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'")
'';
}
+94
View File
@@ -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
+65
View File
@@ -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 ];
};
})