diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index e7a898f51af45..087abfed1032c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1531,6 +1531,7 @@ ./services/web-apps/whitebophir.nix ./services/web-apps/wiki-js.nix ./services/web-apps/windmill.nix + ./services/web-apps/wizarr.nix ./services/web-apps/wordpress.nix ./services/web-apps/writefreely.nix ./services/web-apps/your_spotify.nix diff --git a/nixos/modules/services/web-apps/wizarr.nix b/nixos/modules/services/web-apps/wizarr.nix new file mode 100644 index 0000000000000..88a5318da9bc7 --- /dev/null +++ b/nixos/modules/services/web-apps/wizarr.nix @@ -0,0 +1,105 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.wizarr; +in +{ + options.services.wizarr = { + enable = lib.mkEnableOption "Wizarr, an advanced user invitation and management system for Jellyfin, Plex, Emby etc."; + + host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = '' + Host to bind Wizarr to. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 5960; + description = '' + Port to bind Wizarr to. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + example = true; + description = '' + Whether to open the port in the firewall for Wizarr. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.wizarr = { + description = "Wizarr - user invitation and management system"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment.DATABASE_DIR = "/var/lib/wizarr"; + + script = '' + ${lib.getExe pkgs.wizarr} -D \ + -b 0.0.0.0:${toString cfg.port} \ + --workers 3 \ + --log-level=info + ''; + + serviceConfig = { + Type = "forking"; + + DynamicUser = true; + User = "wizarr"; + RuntimeDirectory = "wizarr"; + StateDirectory = "wizarr"; + + # Hardening + ProtectClock = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + ProtectHostname = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectHome = true; + ProcSubset = "pid"; + + PrivateTmp = true; + PrivateNetwork = false; + PrivateUsers = cfg.port >= 1024; + PrivateDevices = true; + + RestrictRealtime = true; + RestrictNamespaces = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + + MemoryDenyWriteExecute = false; # Java does not like w^x :( + LockPersonality = true; + AmbientCapabilities = lib.optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE"; + CapabilityBoundingSet = ""; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@resources" + "~@privileged" + ]; + UMask = "0027"; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index eb15a5f874e1d..e26fa0fc3e4f6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1120,6 +1120,7 @@ in { wireguard = handleTest ./wireguard {}; wg-access-server = handleTest ./wg-access-server.nix {}; without-nix = handleTest ./without-nix.nix {}; + wizarr = handleTest ./web-apps/wizarr.nix {}; wmderland = handleTest ./wmderland.nix {}; workout-tracker = handleTest ./workout-tracker.nix {}; wpa_supplicant = import ./wpa_supplicant.nix { inherit pkgs runTest; }; diff --git a/nixos/tests/web-apps/wizarr.nix b/nixos/tests/web-apps/wizarr.nix new file mode 100644 index 0000000000000..3ae101df7c3f1 --- /dev/null +++ b/nixos/tests/web-apps/wizarr.nix @@ -0,0 +1,27 @@ +import ../make-test-python.nix ( + { lib, pkgs, ... }: + { + name = "wizarr"; + + meta.maintainers = with lib.maintainers; [ pluiedev ]; + + nodes.machine = + { ... }: + { + services.wizarr = { + enable = true; + }; + }; + + testScript = '' + import json + + machine.start() + machine.wait_for_unit("wizarr.target") + machine.wait_until_succeeds("journalctl --since -1m --unit wizarr --grep Listening") + + assert {'status': 'online'} == json.loads(machine.succeed("curl http://localhost:5000")) + + ''; + } +) diff --git a/pkgs/by-name/wi/wizarr/package.nix b/pkgs/by-name/wi/wizarr/package.nix new file mode 100644 index 0000000000000..aa4b767189576 --- /dev/null +++ b/pkgs/by-name/wi/wizarr/package.nix @@ -0,0 +1,165 @@ +{ + lib, + python3, + fetchFromGitHub, + buildNpmPackage, + nodejs, + pkg-config, + faketty, + vips, +}: +let + version = "4.1.1"; + + src = fetchFromGitHub { + owner = "wizarrrr"; + repo = "wizarr"; + rev = "refs/tags/v4.1.1"; + hash = "sha256-a5rqrN5wxK7YwO4vsJg9KJx9LckF/g5Z0YGf1K05/Ns="; + }; +in +python3.pkgs.buildPythonApplication { + pname = "wizarr"; + inherit version src; + pyproject = true; + + sourceRoot = "${src.name}/apps/wizarr-backend"; + + # Remove arbitrary limitation of the server only being accessible via 127.0.0.1:5000 + postPatch = '' + sed -i '/"127.0.0.1:5000"$/d' wizarr_backend/app/config.py + ''; + + nativeBuildInputs = with python3.pkgs; [ + pythonRelaxDepsHook + wrapPython + ]; + + build-system = with python3.pkgs; [ + poetry-core + ]; + + dependencies = with python3.pkgs; [ + apscheduler + coloredlogs + cryptography + python-dotenv + flask + flask-caching + flask-jwt-extended + flask-restx + flask-session + flask-socketio + python-nmap + packaging + peewee + plexapi + psutil + pytz + requests + schematics + tabulate + termcolor + webauthn + werkzeug + sentry-sdk + gunicorn + gevent + gevent-websocket + pydantic + requests-cache + flask-apscheduler + password-strength + ]; + + # Needs to be added to the PYTHONPATH for gunicorn + propagatedBuildInputs = with python3.pkgs; [ gevent-websocket ]; + + pythonRemoveDeps = [ + # Unmaintained and unused + "flask-oauthlib" + ]; + + pythonRelaxDeps = [ + "cryptography" + "flask" + "flask-session" + "gevent" + "gunicorn" + "packaging" + "psutil" + "pydantic" + "pytz" + "webauthn" + "password-strength" + ]; + + postInstall = '' + install -Dm644 ${src}/latest -t $out/share/wizarr + + # The backend scripts expect modules like app, api, etc. to be on the *top-level* instead of + # being under wizarr_backend. (i.e. imports look like `from app` instead of `from wizarr_backend.app`). + # Instead of patching every file, we just move them out to be directly underneath site packages. + (cd $out/${python3.sitePackages}; mv wizarr_backend/* .) + ''; + + # Test points at nonexistent module + doCheck = false; + + postFixup = '' + mkdir -p $out/bin + + makeWrapper ${lib.getExe python3.pkgs.gunicorn} $out/bin/wizarr \ + --prefix PATH : "$program_PATH" \ + --set PYTHONPATH "$program_PYTHONPATH" \ + --set LATEST_FILE "$out/share/wizarr/latest" \ + --add-flags "-k geventwebsocket.gunicorn.workers.GeventWebSocketWorker" \ + --add-flags "-m 007 run:app" + ''; + + pythonImportsCheck = [ "wizarr_backend" ]; + + passthru.frontend = buildNpmPackage { + pname = "wizarr-frontend"; + inherit version src; + + npmDepsHash = "sha256-IV/xzBrgisbMRCE1cDNoBjRAbHq3Wz2D1UFQl0EPbW0="; + + makeCacheWritable = true; + + nativeBuildInputs = [ + nodejs + pkg-config + faketty + ]; + + buildInputs = [ vips ]; + + # Avoid running postinstall which calls `nx run wizarr-backend:install` + npmFlags = [ "--ignore-script" ]; + + env.CYPRESS_INSTALL_BINARY = "0"; + + buildPhase = '' + runHook preBuild + faketty npm run build -w @wizarrrr/wizarr-frontend + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + cp -r dist/apps/wizarr-frontend $out + runHook postInstall + ''; + }; + + meta = { + description = "Advanced user invitation and management system for Jellyfin, Plex, Emby etc."; + homepage = "https://wizarr.dev/"; + changelog = "https://github.com/wizarrrr/wizarr/blob/${src.rev}/CHANGELOG.md"; + license = with lib.licenses; [ mit ]; + maintainers = with lib.maintainers; [ pluiedev ]; + platforms = with lib.platforms; linux ++ darwin ++ windows; + mainProgram = "wizarr"; + }; +} diff --git a/pkgs/development/python-modules/flask-apscheduler/default.nix b/pkgs/development/python-modules/flask-apscheduler/default.nix new file mode 100644 index 0000000000000..f5f78f889be45 --- /dev/null +++ b/pkgs/development/python-modules/flask-apscheduler/default.nix @@ -0,0 +1,55 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + pythonOlder, + + setuptools, + + flask, + apscheduler, + python-dateutil, + + pytestCheckHook, +}: +let + version = "1.13.1"; + + src = fetchFromGitHub { + owner = "viniciuschiele"; + repo = "flask-apscheduler"; + rev = "refs/tags/${version}"; + hash = "sha256-0gZueUuBBpKGWE6OCJiJL/EEIMqCVc3hgLKwIWFuSZI="; + }; +in +buildPythonPackage { + pname = "flask-apscheduler"; + inherit version src; + pyproject = true; + + disabled = pythonOlder "3.8"; + + build-system = [ + setuptools + ]; + + dependencies = [ + flask + apscheduler + python-dateutil + ]; + + pythonImportsCheck = [ "flask_apscheduler" ]; + + nativeCheckInputs = [ + pytestCheckHook + ]; + + meta = { + description = "Adds APScheduler support to Flask"; + homepage = "https://github.com/viniciuschiele/flask-apscheduler"; + changelog = "https://github.com/viniciuschiele/flask-apscheduler/blob/${src.rev}/CHANGELOG.md"; + license = with lib.licenses; [ asl20 ]; + maintainers = with lib.maintainers; [ pluiedev ]; + }; +} diff --git a/pkgs/development/python-modules/mo-installer/default.nix b/pkgs/development/python-modules/mo-installer/default.nix new file mode 100644 index 0000000000000..2b4988b87e0e4 --- /dev/null +++ b/pkgs/development/python-modules/mo-installer/default.nix @@ -0,0 +1,36 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools-scm, +}: +let + version = "0.3.0"; + + src = fetchFromGitHub { + owner = "schematics"; + repo = "schematics"; + rev = "refs/tags/${version}"; + hash = "sha256-xlA09D2sNN6/XAGGBL8YhuEkCxfty3TU2mosK1H5Zd0="; + }; +in +buildPythonPackage { + pname = "mo-installer"; + inherit version src; + pyproject = true; + + build-system = [ setuptools-scm ]; + + pythonImportsCheck = [ "mo_installer" ]; + + # Both checks fail and I don't know why + doCheck = false; + + meta = { + description = "Help to install gettext mo file in a setuptools package"; + homepage = "https://github.com/s-ball/mo_installer"; + changelog = "https://github.com/s-ball/mo_installer/blob/${src.rev}/CHANGES.txt"; + license = with lib.licenses; [ mit ]; + maintainers = with lib.maintainers; [ pluiedev ]; + }; +} diff --git a/pkgs/development/python-modules/password-strength/default.nix b/pkgs/development/python-modules/password-strength/default.nix new file mode 100644 index 0000000000000..33b109538df24 --- /dev/null +++ b/pkgs/development/python-modules/password-strength/default.nix @@ -0,0 +1,44 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + setuptools, + six, + unittestCheckHook, +}: +let + version = "0.0.3"; + + src = fetchFromGitHub { + owner = "kolypto"; + repo = "py-password-strength"; + rev = "refs/tags/v${version}"; + hash = "sha256-8zjyo0jC4PxFJxM0VZ/u29heqqO5UbkOKAVxtNkcb7U="; + }; +in +buildPythonPackage { + pname = "password-strength"; + inherit version src; + pyproject = true; + + build-system = [ setuptools ]; + + dependencies = [ six ]; + + pythonImportsCheck = [ "password_strength" ]; + + # nativeCheckInputs = [ unittestCheckHook ]; + # + # unittestFlagsArray = [ + # "-s" + # "tests" + # ]; + + meta = { + description = "Password strength and validation."; + homepage = "https://github.com/kolypto/py-password-strength"; + changelog = "https://github.com/kolypto/py-password-strength/blob/${src.rev}/CHANGES.txt"; + license = with lib.licenses; [ mit ]; + maintainers = with lib.maintainers; [ pluiedev ]; + }; +} diff --git a/pkgs/development/python-modules/schematics/default.nix b/pkgs/development/python-modules/schematics/default.nix new file mode 100644 index 0000000000000..fbef7daab49bc --- /dev/null +++ b/pkgs/development/python-modules/schematics/default.nix @@ -0,0 +1,57 @@ +{ + lib, + buildPythonPackage, + fetchFromGitHub, + + setuptools, + mo-installer, + + pytestCheckHook, + python-dateutil, + mock, + pymongo, + coverage, +}: +let + version = "2.1.1"; + + src = fetchFromGitHub { + owner = "schematics"; + repo = "schematics"; + rev = "refs/tags/v${version}"; + hash = "sha256-jclKcX/4QbRCuWKdKq97Wo3q10Rbkt7/R8AJQTwnwVk="; + }; +in +buildPythonPackage { + pname = "schematics"; + inherit version src; + pyproject = true; + + postPatch = '' + substituteInPlace setup.py \ + --replace-fail "'pytest-runner'," "" + ''; + + build-system = [ + setuptools + mo-installer + ]; + + pythonImportsCheck = [ "schematics" ]; + + nativeCheckInputs = [ + pytestCheckHook + python-dateutil + mock + pymongo + coverage + ]; + + meta = { + description = "Python Data Structures for Humans"; + homepage = "https://schematics.readthedocs.io/"; + changelog = "https://github.com/s-ball/mo_installer/blob/${src.rev}/HISTORY.rst"; + license = with lib.licenses; [ bsd3 ]; + maintainers = with lib.maintainers; [ pluiedev ]; + }; +} diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix index c039874560460..7b60728aa9d83 100644 --- a/pkgs/top-level/python-packages.nix +++ b/pkgs/top-level/python-packages.nix @@ -4600,6 +4600,8 @@ self: super: with self; { flask-appbuilder = callPackage ../development/python-modules/flask-appbuilder { }; + flask-apscheduler = callPackage ../development/python-modules/flask-apscheduler { }; + flask-assets = callPackage ../development/python-modules/flask-assets { }; flask-babel = callPackage ../development/python-modules/flask-babel { }; @@ -8267,6 +8269,8 @@ self: super: with self; { molbar = callPackage ../development/python-modules/molbar { }; + mo-installer = callPackage ../development/python-modules/mo-installer { }; + molecule = callPackage ../development/python-modules/molecule { }; molecule-plugins = callPackage ../development/python-modules/molecule/plugins.nix { }; @@ -9888,6 +9892,8 @@ self: super: with self; { passlib = callPackage ../development/python-modules/passlib { }; + password-strength = callPackage ../development/python-modules/password-strength { }; + paste = callPackage ../development/python-modules/paste { }; pastedeploy = callPackage ../development/python-modules/pastedeploy { }; @@ -14222,6 +14228,8 @@ self: super: with self; { schema-salad = callPackage ../development/python-modules/schema-salad { }; + schematics = callPackage ../development/python-modules/schematics { }; + schemdraw = callPackage ../development/python-modules/schemdraw { }; schiene = callPackage ../development/python-modules/schiene { };