Commit 62780717 authored by Tim Van Baak's avatar Tim Van Baak
Browse files

nixos/immich-public-proxy: init module

parent 6a64387c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -77,6 +77,8 @@

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

- [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).

- [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).

- [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).
+1 −0
Original line number Diff line number Diff line
@@ -1484,6 +1484,7 @@
  ./services/web-apps/icingaweb2/module-monitoring.nix
  ./services/web-apps/ifm.nix
  ./services/web-apps/immich.nix
  ./services/web-apps/immich-public-proxy.nix
  ./services/web-apps/invidious.nix
  ./services/web-apps/invoiceplane.nix
  ./services/web-apps/isso.nix
+98 −0
Original line number Diff line number Diff line
{
  config,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.services.immich-public-proxy;
  format = pkgs.formats.json { };
  inherit (lib)
    types
    mkIf
    mkOption
    mkEnableOption
    ;
in
{
  options.services.immich-public-proxy = {
    enable = mkEnableOption "Immich Public Proxy";
    package = lib.mkPackageOption pkgs "immich-public-proxy" { };

    immichUrl = mkOption {
      type = types.str;
      description = "URL of the Immich instance";
    };

    port = mkOption {
      type = types.port;
      default = 3000;
      description = "The port that IPP will listen on.";
    };
    openFirewall = mkOption {
      type = types.bool;
      default = false;
      description = "Whether to open the IPP port in the firewall";
    };

    settings = mkOption {
      type = types.submodule {
        freeformType = format.type;
      };
      default = { };
      description = ''
        Configuration for IPP. See <https://github.com/alangrainger/immich-public-proxy/blob/main/README.md#additional-configuration> for options and defaults.
      '';
    };
  };

  config = mkIf cfg.enable {
    systemd.services.immich-public-proxy = {
      description = "Immich public proxy for sharing albums publicly without exposing your Immich instance";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      environment = {
        IMMICH_URL = cfg.immichUrl;
        IPP_PORT = builtins.toString cfg.port;
        IPP_CONFIG = "${format.generate "config.json" cfg.settings}";
      };
      serviceConfig = {
        ExecStart = lib.getExe cfg.package;
        SyslogIdentifier = "ipp";
        User = "ipp";
        Group = "ipp";
        DynamicUser = true;
        Type = "simple";
        Restart = "on-failure";
        RestartSec = 3;

        # Hardening
        CapabilityBoundingSet = "";
        NoNewPrivileges = true;
        PrivateUsers = true;
        PrivateTmp = true;
        PrivateDevices = true;
        PrivateMounts = true;
        ProtectClock = true;
        ProtectControlGroups = true;
        ProtectHome = true;
        ProtectHostname = true;
        ProtectKernelLogs = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        RestrictAddressFamilies = [
          "AF_INET"
          "AF_INET6"
          "AF_UNIX"
        ];
        RestrictNamespaces = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
      };
    };

    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];

    meta.maintainers = with lib.maintainers; [ jaculabilis ];
  };
}
+1 −0
Original line number Diff line number Diff line
@@ -467,6 +467,7 @@ in {
  ifm = handleTest ./ifm.nix {};
  iftop = handleTest ./iftop.nix {};
  immich = handleTest ./web-apps/immich.nix {};
  immich-public-proxy = handleTest ./web-apps/immich-public-proxy.nix {};
  incron = handleTest ./incron.nix {};
  incus = pkgs.recurseIntoAttrs (handleTest ./incus { lts = false; inherit system pkgs; });
  incus-lts = pkgs.recurseIntoAttrs (handleTest ./incus { inherit system pkgs; });
+105 −0
Original line number Diff line number Diff line
import ../make-test-python.nix (
  { pkgs, lib, ... }:
  {
    name = "immich-public-proxy";

    nodes.machine =
      { pkgs, ... }@args:
      {
        environment.systemPackages = [
          pkgs.imagemagick
          pkgs.immich-cli
        ];
        services.immich = {
          enable = true;
          port = 2283;
          # disable a lot of features that aren't needed for this test
          machine-learning.enable = false;
          settings = {
            backup.database.enabled = false;
            machineLearning.enabled = false;
            map.enabled = false;
            reverseGeocoding.enabled = false;
            metadata.faces.import = false;
            newVersionCheck.enabled = false;
            notifications.smtp.enabled = false;
          };
        };
        services.immich-public-proxy = {
          enable = true;
          immichUrl = "http://localhost:2283";
          port = 8002;
          settings.ipp.responseHeaders."X-NixOS" = "Rules";
        };
      };

    testScript = ''
      import json

      machine.wait_for_unit("immich-server.service")
      machine.wait_for_unit("immich-public-proxy.service")
      machine.wait_for_open_port(2283)
      machine.wait_for_open_port(8002)

      # The proxy should be up
      machine.succeed("curl -sf http://localhost:8002")

      # Verify the static assets are served
      machine.succeed("curl -sf http://localhost:8002/robots.txt")
      machine.succeed("curl -sf http://localhost:8002/share/static/style.css")

      # Check that the response header in the settings is sent
      res = machine.succeed("""
        curl -sD - http://localhost:8002 -o /dev/null
      """)
      assert "x-nixos: rules" in res.lower(), res

      # Log in to Immich and create an access key
      machine.succeed("""
        curl -sf --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up
      """)
      res = machine.succeed("""
        curl -sf --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login
      """)
      token = json.loads(res)['accessToken']
      res = machine.succeed("""
        curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys
      """ % token)
      key = json.loads(res)['secret']
      machine.succeed(f"immich login http://localhost:2283/api {key}")
      res = machine.succeed("immich server-info")
      print(res)

      # Upload some blank images to a new album
      # If there's only one image, the proxy serves the image directly
      machine.succeed("magick -size 800x600 canvas:white /tmp/white.png")
      machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/white.png")
      machine.succeed("magick -size 800x600 canvas:black /tmp/black.png")
      machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/black.png")
      res = machine.succeed("immich server-info")
      print(res)

      # Get the new album id
      res = machine.succeed("""
        curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums
      """ % token)
      album_id = json.loads(res)[0]['id']

      # Create a shared link
      res = machine.succeed("""
        curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links
      """ % (token, album_id))
      share_key = json.loads(res)['key']

      # Access the share
      machine.succeed("""
        curl -sf http://localhost:2283/share/%s
      """ % share_key)

      # Access the share through the proxy
      machine.succeed("""
        curl -sf http://localhost:8002/share/%s
      """ % share_key)
    '';
  }
)
Loading