Skip to content

Commit 64e0c30

Browse files
authored
immich-public-proxy: init at 1.5.4, nixos/immich-public-proxy: init module (#362907)
2 parents f6b0b92 + 6278071 commit 64e0c30

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

maintainers/maintainer-list.nix

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10152,6 +10152,12 @@
1015210152
githubId = 45084216;
1015310153
keys = [ { fingerprint = "1BF9 8D10 E0D0 0B41 5723 5836 4C13 3A84 E646 9228"; } ];
1015410154
};
10155+
jaculabilis = {
10156+
name = "Tim Van Baak";
10157+
email = "[email protected]";
10158+
github = "Jaculabilis";
10159+
githubId = 10787844;
10160+
};
1015510161
jaduff = {
1015610162
email = "[email protected]";
1015710163
github = "jaduff";

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181

8282
- [Actual Budget](https://actualbudget.org/), a local-first personal finance app. Available as [services.actual](#opt-services.actual.enable).
8383

84+
- [immich-public-proxy](https://github.com/alangrainger/immich-public-proxy), a proxy for sharing Immich albums without exposing the Immich API. Available as [services.immich-public-proxy](#opt-services.immich-public-proxy.enable).
85+
8486
- [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable).
8587

8688
- [nvidia-gpu](https://github.com/utkuozdemir/nvidia_gpu_exporter), a Prometheus exporter that scrapes `nvidia-smi` for GPU metrics. Available as [services.prometheus.exporters.nvidia-gpu](#opt-services.prometheus.exporters.nvidia-gpu.enable).

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,7 @@
14861486
./services/web-apps/icingaweb2/module-monitoring.nix
14871487
./services/web-apps/ifm.nix
14881488
./services/web-apps/immich.nix
1489+
./services/web-apps/immich-public-proxy.nix
14891490
./services/web-apps/invidious.nix
14901491
./services/web-apps/invoiceplane.nix
14911492
./services/web-apps/isso.nix
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
let
8+
cfg = config.services.immich-public-proxy;
9+
format = pkgs.formats.json { };
10+
inherit (lib)
11+
types
12+
mkIf
13+
mkOption
14+
mkEnableOption
15+
;
16+
in
17+
{
18+
options.services.immich-public-proxy = {
19+
enable = mkEnableOption "Immich Public Proxy";
20+
package = lib.mkPackageOption pkgs "immich-public-proxy" { };
21+
22+
immichUrl = mkOption {
23+
type = types.str;
24+
description = "URL of the Immich instance";
25+
};
26+
27+
port = mkOption {
28+
type = types.port;
29+
default = 3000;
30+
description = "The port that IPP will listen on.";
31+
};
32+
openFirewall = mkOption {
33+
type = types.bool;
34+
default = false;
35+
description = "Whether to open the IPP port in the firewall";
36+
};
37+
38+
settings = mkOption {
39+
type = types.submodule {
40+
freeformType = format.type;
41+
};
42+
default = { };
43+
description = ''
44+
Configuration for IPP. See <https://github.com/alangrainger/immich-public-proxy/blob/main/README.md#additional-configuration> for options and defaults.
45+
'';
46+
};
47+
};
48+
49+
config = mkIf cfg.enable {
50+
systemd.services.immich-public-proxy = {
51+
description = "Immich public proxy for sharing albums publicly without exposing your Immich instance";
52+
after = [ "network.target" ];
53+
wantedBy = [ "multi-user.target" ];
54+
environment = {
55+
IMMICH_URL = cfg.immichUrl;
56+
IPP_PORT = builtins.toString cfg.port;
57+
IPP_CONFIG = "${format.generate "config.json" cfg.settings}";
58+
};
59+
serviceConfig = {
60+
ExecStart = lib.getExe cfg.package;
61+
SyslogIdentifier = "ipp";
62+
User = "ipp";
63+
Group = "ipp";
64+
DynamicUser = true;
65+
Type = "simple";
66+
Restart = "on-failure";
67+
RestartSec = 3;
68+
69+
# Hardening
70+
CapabilityBoundingSet = "";
71+
NoNewPrivileges = true;
72+
PrivateUsers = true;
73+
PrivateTmp = true;
74+
PrivateDevices = true;
75+
PrivateMounts = true;
76+
ProtectClock = true;
77+
ProtectControlGroups = true;
78+
ProtectHome = true;
79+
ProtectHostname = true;
80+
ProtectKernelLogs = true;
81+
ProtectKernelModules = true;
82+
ProtectKernelTunables = true;
83+
RestrictAddressFamilies = [
84+
"AF_INET"
85+
"AF_INET6"
86+
"AF_UNIX"
87+
];
88+
RestrictNamespaces = true;
89+
RestrictRealtime = true;
90+
RestrictSUIDSGID = true;
91+
};
92+
};
93+
94+
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
95+
96+
meta.maintainers = with lib.maintainers; [ jaculabilis ];
97+
};
98+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ in {
468468
ifm = handleTest ./ifm.nix {};
469469
iftop = handleTest ./iftop.nix {};
470470
immich = handleTest ./web-apps/immich.nix {};
471+
immich-public-proxy = handleTest ./web-apps/immich-public-proxy.nix {};
471472
incron = handleTest ./incron.nix {};
472473
incus = pkgs.recurseIntoAttrs (handleTest ./incus { lts = false; inherit system pkgs; });
473474
incus-lts = pkgs.recurseIntoAttrs (handleTest ./incus { inherit system pkgs; });
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import ../make-test-python.nix (
2+
{ pkgs, lib, ... }:
3+
{
4+
name = "immich-public-proxy";
5+
6+
nodes.machine =
7+
{ pkgs, ... }@args:
8+
{
9+
environment.systemPackages = [
10+
pkgs.imagemagick
11+
pkgs.immich-cli
12+
];
13+
services.immich = {
14+
enable = true;
15+
port = 2283;
16+
# disable a lot of features that aren't needed for this test
17+
machine-learning.enable = false;
18+
settings = {
19+
backup.database.enabled = false;
20+
machineLearning.enabled = false;
21+
map.enabled = false;
22+
reverseGeocoding.enabled = false;
23+
metadata.faces.import = false;
24+
newVersionCheck.enabled = false;
25+
notifications.smtp.enabled = false;
26+
};
27+
};
28+
services.immich-public-proxy = {
29+
enable = true;
30+
immichUrl = "http://localhost:2283";
31+
port = 8002;
32+
settings.ipp.responseHeaders."X-NixOS" = "Rules";
33+
};
34+
};
35+
36+
testScript = ''
37+
import json
38+
39+
machine.wait_for_unit("immich-server.service")
40+
machine.wait_for_unit("immich-public-proxy.service")
41+
machine.wait_for_open_port(2283)
42+
machine.wait_for_open_port(8002)
43+
44+
# The proxy should be up
45+
machine.succeed("curl -sf http://localhost:8002")
46+
47+
# Verify the static assets are served
48+
machine.succeed("curl -sf http://localhost:8002/robots.txt")
49+
machine.succeed("curl -sf http://localhost:8002/share/static/style.css")
50+
51+
# Check that the response header in the settings is sent
52+
res = machine.succeed("""
53+
curl -sD - http://localhost:8002 -o /dev/null
54+
""")
55+
assert "x-nixos: rules" in res.lower(), res
56+
57+
# Log in to Immich and create an access key
58+
machine.succeed("""
59+
curl -sf --json '{ "email": "[email protected]", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
60+
""")
61+
res = machine.succeed("""
62+
curl -sf --json '{ "email": "[email protected]", "password": "admin" }' http://localhost:2283/api/auth/login
63+
""")
64+
token = json.loads(res)['accessToken']
65+
res = machine.succeed("""
66+
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
67+
""" % token)
68+
key = json.loads(res)['secret']
69+
machine.succeed(f"immich login http://localhost:2283/api {key}")
70+
res = machine.succeed("immich server-info")
71+
print(res)
72+
73+
# Upload some blank images to a new album
74+
# If there's only one image, the proxy serves the image directly
75+
machine.succeed("magick -size 800x600 canvas:white /tmp/white.png")
76+
machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/white.png")
77+
machine.succeed("magick -size 800x600 canvas:black /tmp/black.png")
78+
machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/black.png")
79+
res = machine.succeed("immich server-info")
80+
print(res)
81+
82+
# Get the new album id
83+
res = machine.succeed("""
84+
curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums
85+
""" % token)
86+
album_id = json.loads(res)[0]['id']
87+
88+
# Create a shared link
89+
res = machine.succeed("""
90+
curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links
91+
""" % (token, album_id))
92+
share_key = json.loads(res)['key']
93+
94+
# Access the share
95+
machine.succeed("""
96+
curl -sf http://localhost:2283/share/%s
97+
""" % share_key)
98+
99+
# Access the share through the proxy
100+
machine.succeed("""
101+
curl -sf http://localhost:8002/share/%s
102+
""" % share_key)
103+
'';
104+
}
105+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
lib,
3+
buildNpmPackage,
4+
fetchFromGitHub,
5+
nix-update-script,
6+
nixosTests,
7+
nodejs,
8+
}:
9+
buildNpmPackage rec {
10+
pname = "immich-public-proxy";
11+
version = "1.5.4";
12+
src = fetchFromGitHub {
13+
owner = "alangrainger";
14+
repo = "immich-public-proxy";
15+
rev = "v${version}";
16+
hash = "sha256-GoAUR8s2tRHpXD/yk42u6DDvkI97XAUlF9Zsq8pb/1M=";
17+
};
18+
19+
sourceRoot = "${src.name}/app";
20+
21+
npmDepsHash = "sha256-BN7g+31ijH8r9rsv5zzjnE8PT7ozAswoyZNJ0XqXGyw=";
22+
23+
# patch in absolute nix store paths so the process doesn't need to cwd in $out
24+
postPatch = ''
25+
substituteInPlace src/index.ts --replace-fail \
26+
"const app = express()" \
27+
"const app = express()
28+
// Set the views path to the nix output
29+
app.set('views', '$out/lib/node_modules/immich-public-proxy/views')" \
30+
--replace-fail \
31+
"static('public'" \
32+
"static('$out/lib/node_modules/immich-public-proxy/public'"
33+
'';
34+
35+
passthru = {
36+
tests = {
37+
inherit (nixosTests) immich-public-proxy;
38+
};
39+
updateScript = nix-update-script { };
40+
};
41+
42+
meta = {
43+
description = "Share your Immich photos and albums in a safe way without exposing your Immich instance to the public";
44+
homepage = "https://github.com/alangrainger/immich-public-proxy";
45+
license = lib.licenses.agpl3Only;
46+
maintainers = with lib.maintainers; [ jaculabilis ];
47+
inherit (nodejs.meta) platforms;
48+
mainProgram = "immich-public-proxy";
49+
};
50+
}

0 commit comments

Comments
 (0)