From 97b95b69b9dc669313019eff33a6de8020d23f0c Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 10:17:43 +0100 Subject: [PATCH 01/30] fix: N+1 query --- humitifier-server/src/hosts/models.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/humitifier-server/src/hosts/models.py b/humitifier-server/src/hosts/models.py index 6bc4497..db3582b 100644 --- a/humitifier-server/src/hosts/models.py +++ b/humitifier-server/src/hosts/models.py @@ -134,15 +134,32 @@ def regenerate_alerts(self): @property def num_critical_alerts(self): - return self.alerts.filter(level=AlertLevel.CRITICAL).count() + return self._get_alerts_for_level(AlertLevel.CRITICAL, count=True) @property def num_warning_alerts(self): - return self.alerts.filter(level=AlertLevel.WARNING).count() + return self._get_alerts_for_level(AlertLevel.WARNING, count=True) @property def num_info_alerts(self): - return self.alerts.filter(level=AlertLevel.INFO).count() + return self._get_alerts_for_level(AlertLevel.INFO, count=True) + + def _get_alerts_for_level(self, level, count=False): + # Use the prefetched objects if they are available + # It's not quicker for a single query, but it is for multiple queries + # (Read: the list page) + if 'alerts' in self._prefetched_objects_cache: + alerts = [alert for alert in self.alerts.all() if (alert.level == + level)] + if count: + return len(alerts) + return alerts + + qs = self.alerts.filter(level=level) + + if count: + return qs.count() + return qs ## ## Display methods From d03a63de95e3c47ee0caad3b56b9ba52d3a1c245 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 10:18:12 +0100 Subject: [PATCH 02/30] fix: run server using exec --- humitifier-server/docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/humitifier-server/docker/entrypoint.sh b/humitifier-server/docker/entrypoint.sh index 2d263bc..49c4daa 100644 --- a/humitifier-server/docker/entrypoint.sh +++ b/humitifier-server/docker/entrypoint.sh @@ -7,4 +7,4 @@ source /app/.venv/bin/activate python src/manage.py migrate # Run da server -gunicorn humitifier_server.wsgi:application -c gunicorn.conf.py "$@" \ No newline at end of file +exec gunicorn humitifier_server.wsgi:application -c gunicorn.conf.py "$@" \ No newline at end of file From 4170fc738c47ef742ea16fd414ef64a4d53747c4 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 13:22:37 +0100 Subject: [PATCH 03/30] feat: OIDC login support --- humitifier-server/poetry.lock | 194 +++++++++++++++++- humitifier-server/pyproject.toml | 1 + .../src/humitifier_server/oidc_backend.py | 35 ++++ .../src/humitifier_server/settings.py | 67 ++++++ .../src/humitifier_server/urls.py | 20 +- .../templates/base/page_parts/header.html | 10 +- 6 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 humitifier-server/src/humitifier_server/oidc_backend.py diff --git a/humitifier-server/poetry.lock b/humitifier-server/poetry.lock index 13d0d4c..82f0f6a 100644 --- a/humitifier-server/poetry.lock +++ b/humitifier-server/poetry.lock @@ -121,6 +121,85 @@ files = [ {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -260,6 +339,55 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "curtsies" version = "0.4.2" @@ -561,6 +689,24 @@ files = [ [package.dependencies] ansicon = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "josepy" +version = "1.14.0" +description = "JOSE protocol implementation in Python" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "josepy-1.14.0-py3-none-any.whl", hash = "sha256:d2b36a30f316269f3242f4c2e45e15890784178af5ec54fa3e49cf9234ee22e0"}, + {file = "josepy-1.14.0.tar.gz", hash = "sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd"}, +] + +[package.dependencies] +cryptography = ">=1.5" +pyopenssl = ">=0.13" + +[package.extras] +docs = ["sphinx (>=4.3.0)", "sphinx-rtd-theme (>=1.0)"] + [[package]] name = "markdown" version = "3.7" @@ -576,6 +722,23 @@ files = [ docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] +[[package]] +name = "mozilla-django-oidc" +version = "4.0.1" +description = "A lightweight authentication and access management library for integration with OpenID Connect enabled authentication services." +optional = false +python-versions = "*" +files = [ + {file = "mozilla-django-oidc-4.0.1.tar.gz", hash = "sha256:4ff8c64069e3e05c539cecf9345e73225a99641a25e13b7a5f933ec897b58918"}, + {file = "mozilla_django_oidc-4.0.1-py2.py3-none-any.whl", hash = "sha256:04ef58759be69f22cdc402d082480aaebf193466cad385dc9e4f8df2a0b187ca"}, +] + +[package.dependencies] +cryptography = "*" +Django = ">=3.2" +josepy = "*" +requests = "*" + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -719,6 +882,17 @@ files = [ {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pygments" version = "2.18.0" @@ -733,6 +907,24 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyopenssl" +version = "24.2.1" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, + {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, +] + +[package.dependencies] +cryptography = ">=41.0.5,<44" + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -925,4 +1117,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "9b90c3ab84c9ce2596c31f902fd6f3f9c7d99f57ce6d8c7bab0344ee85a5de88" +content-hash = "f2a3fb3936f903bd3481633a38f004156863e773f1ca3457c38f0d24f8f4b157" diff --git a/humitifier-server/pyproject.toml b/humitifier-server/pyproject.toml index 0eefb52..7ede8c4 100644 --- a/humitifier-server/pyproject.toml +++ b/humitifier-server/pyproject.toml @@ -16,6 +16,7 @@ django-debug-toolbar = "^4.4.6" django-simple-menu = "^2.1.3" whitenoise = "^6.8.2" sentry-sdk = {extras = ["django"], version = "^2.17.0"} +mozilla-django-oidc = "^4.0.1" [tool.poetry.group.dev.dependencies] diff --git a/humitifier-server/src/humitifier_server/oidc_backend.py b/humitifier-server/src/humitifier_server/oidc_backend.py new file mode 100644 index 0000000..75f1180 --- /dev/null +++ b/humitifier-server/src/humitifier_server/oidc_backend.py @@ -0,0 +1,35 @@ +from mozilla_django_oidc.auth import OIDCAuthenticationBackend + + +class HumitifierOIDCAuthenticationBackend(OIDCAuthenticationBackend): + + def create_user(self, claims): + email = claims.get('email') + first_name = claims.get('given_name') + last_name = claims.get('family_name') + username = self.get_username(claims) + + return self.UserModel.objects.create_user(username, email=email, + first_name=first_name, last_name=last_name) + + def filter_users_by_claims(self, claims): + username = self.get_username(claims) + if not username: + return self.UserModel.objects.none() + return self.UserModel.objects.filter(username=username) + + def get_username(self, claims): + return claims.get('preferred_username') + + def update_user(self, user, claims): + email = claims.get('email') + first_name = claims.get('given_name') + last_name = claims.get('family_name') + + user.email = email + user.first_name = first_name + user.last_name = last_name + + user.save() + + return user \ No newline at end of file diff --git a/humitifier-server/src/humitifier_server/settings.py b/humitifier-server/src/humitifier_server/settings.py index 1817d34..8ecab2c 100644 --- a/humitifier-server/src/humitifier_server/settings.py +++ b/humitifier-server/src/humitifier_server/settings.py @@ -11,6 +11,11 @@ """ from pathlib import Path +import re + +from django.core.exceptions import ImproperlyConfigured +from rest_framework.reverse import reverse_lazy + from . import env # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -116,6 +121,68 @@ } } +# OpenID Connect + + +LOGIN_REDIRECT_URL = reverse_lazy("main:home") +LOGOUT_REDIRECT_URL = reverse_lazy("main:home") + + +if env.get_boolean("DJANGO_OIDC_ENABLED", default=False): + try: + index = INSTALLED_APPS.index('django.contrib.auth') + INSTALLED_APPS.insert(index + 1, "mozilla_django_oidc") + except ValueError: + raise ImproperlyConfigured( + "Cannot enable OIDC; django.contrib.auth is not enabled" + ) + + MIDDLEWARE.append("mozilla_django_oidc.middleware.SessionRefresh") + + AUTHENTICATION_BACKENDS = [ + "humitifier_server.oidc_backend.HumitifierOIDCAuthenticationBackend", + "django.contrib.auth.backends.ModelBackend", + ] + + OIDC_EXEMPT_URLS = [ + re.compile(r"^api/.*$"), + ] + + OIDC_CREATE_USER = env.get_boolean("OIDC_CREATE_USER", default=False) + OIDC_RP_SCOPES = env.get("OIDC_RP_SCOPES", default="openid email profile") + + OIDC_RP_SIGN_ALGO = env.get("OIDC_RP_SIGN_ALGO", default="RS256") + + if client_id :=env.get("OIDC_RP_CLIENT_ID", default=None): + OIDC_RP_CLIENT_ID = client_id + else: + raise ImproperlyConfigured("OIDC_RP_CLIENT_ID is required") + + if client_secret := env.get("OIDC_RP_CLIENT_SECRET", default=None): + OIDC_RP_CLIENT_SECRET = client_secret + else: + raise ImproperlyConfigured("OIDC_RP_CLIENT_SECRET is required") + + if jwk_endpoint := env.get("OIDC_OP_JWKS_ENDPOINT", default=None): + OIDC_OP_JWKS_ENDPOINT = jwk_endpoint + else: + raise ImproperlyConfigured("OIDC_OP_JWKS_ENDPOINT is required") + + if auth_endpoint := env.get("OIDC_OP_AUTHORIZATION_ENDPOINT", default=None): + OIDC_OP_AUTHORIZATION_ENDPOINT = auth_endpoint + else: + raise ImproperlyConfigured("OIDC_OP_AUTHORIZATION_ENDPOINT is required") + + if token_endpoint := env.get("OIDC_OP_TOKEN_ENDPOINT", default=None): + OIDC_OP_TOKEN_ENDPOINT = token_endpoint + else: + raise ImproperlyConfigured("OIDC_OP_TOKEN_ENDPOINT is required") + + if user_endpoint := env.get("OIDC_OP_USER_ENDPOINT", default=None): + OIDC_OP_USER_ENDPOINT = user_endpoint + else: + raise ImproperlyConfigured("OIDC_OP_USER_ENDPOINT is required") + # Security diff --git a/humitifier-server/src/humitifier_server/urls.py b/humitifier-server/src/humitifier_server/urls.py index 2a989a3..ec59f5c 100644 --- a/humitifier-server/src/humitifier_server/urls.py +++ b/humitifier-server/src/humitifier_server/urls.py @@ -18,6 +18,8 @@ from django.conf import settings from django.contrib import admin from django.urls import include, path +from mozilla_django_oidc.urls import OIDCAuthenticateClass, OIDCCallbackClass +from mozilla_django_oidc.views import OIDCLogoutView urlpatterns = [ path("admin/", admin.site.urls), @@ -31,4 +33,20 @@ urlpatterns = [ path("__debug__/", include(debug_toolbar.urls)), - ] + urlpatterns \ No newline at end of file + ] + urlpatterns + +if hasattr(settings, 'OIDC_RP_CLIENT_ID'): + # Custom OIDC url conf, because we're hijacking an existing RP config + urlpatterns += [ + path( + "redirect_uri/", + OIDCCallbackClass.as_view(), + name="oidc_authentication_callback" + ), + path( + "oidc/authenticate/", + OIDCAuthenticateClass.as_view(), + name="oidc_authentication_init", + ), + path("oidc/logout/", OIDCLogoutView.as_view(), name="oidc_logout"), + ] \ No newline at end of file diff --git a/humitifier-server/src/main/templates/base/page_parts/header.html b/humitifier-server/src/main/templates/base/page_parts/header.html index 0741e6c..a3ee71b 100644 --- a/humitifier-server/src/main/templates/base/page_parts/header.html +++ b/humitifier-server/src/main/templates/base/page_parts/header.html @@ -31,6 +31,14 @@
- Humitifier User + {{ user.get_full_name }}
+ {% if user.is_authenticated %} +
+ {% csrf_token %} + +
+ {% else %} + Login + {% endif %} \ No newline at end of file From 2a0474f411ae6dc5ee4dbfea93d341a746a5ad63 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 13:35:49 +0100 Subject: [PATCH 04/30] feat: optional OIDC session refresh --- humitifier-server/src/humitifier_server/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/humitifier-server/src/humitifier_server/settings.py b/humitifier-server/src/humitifier_server/settings.py index 8ecab2c..8aa9dc4 100644 --- a/humitifier-server/src/humitifier_server/settings.py +++ b/humitifier-server/src/humitifier_server/settings.py @@ -137,7 +137,8 @@ "Cannot enable OIDC; django.contrib.auth is not enabled" ) - MIDDLEWARE.append("mozilla_django_oidc.middleware.SessionRefresh") + if env.get_boolean("DJANGO_OIDC_SESSION_REFRESH", default=False): + MIDDLEWARE.append("mozilla_django_oidc.middleware.SessionRefresh") AUTHENTICATION_BACKENDS = [ "humitifier_server.oidc_backend.HumitifierOIDCAuthenticationBackend", From 3b65a5563d93cc12d698ae9a9f7516f1defa7aaf Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 16:16:10 +0100 Subject: [PATCH 05/30] feat: initial authorization + login logic --- humitifier-server/src/hosts/menus.py | 4 + humitifier-server/src/hosts/models.py | 18 +- humitifier-server/src/hosts/views.py | 19 +- .../src/humitifier_server/settings.py | 5 +- .../src/main/context_processors.py | 3 + humitifier-server/src/main/menus.py | 4 + .../src/main/static/main/css/tailwind.css | 182 +++++++++++++++++- .../templates/base/base_html_template.html | 33 ++++ .../templates/base/base_page_template.html | 46 +---- .../main/templates/registration/login.html | 77 ++++++++ humitifier-server/src/main/urls.py | 3 + humitifier-server/src/main/views.py | 59 +++++- 12 files changed, 396 insertions(+), 57 deletions(-) create mode 100644 humitifier-server/src/main/templates/base/base_html_template.html create mode 100644 humitifier-server/src/main/templates/registration/login.html diff --git a/humitifier-server/src/hosts/menus.py b/humitifier-server/src/hosts/menus.py index 1d809f4..1fc7213 100644 --- a/humitifier-server/src/hosts/menus.py +++ b/humitifier-server/src/hosts/menus.py @@ -9,6 +9,7 @@ weight=10, icon="icons/host.html", separator=True, + check=lambda request: request.user.is_authenticated, ) ) @@ -19,6 +20,7 @@ reverse("hosts:tasks"), weight=10, icon="icons/tasks.html", + check=lambda request: request.user.is_superuser, ) ) @@ -29,6 +31,7 @@ reverse("hosts:scan_profiles"), weight=10, icon="icons/terminal.html", + check=lambda request: request.user.is_superuser, ) ) @@ -39,5 +42,6 @@ reverse("hosts:data_sources"), weight=10, icon="icons/databases.html", + check=lambda request: request.user.is_superuser, ) ) diff --git a/humitifier-server/src/hosts/models.py b/humitifier-server/src/hosts/models.py index db3582b..d7eea56 100644 --- a/humitifier-server/src/hosts/models.py +++ b/humitifier-server/src/hosts/models.py @@ -41,7 +41,14 @@ def _json_value(field: str): class HostManager(models.Manager): def get_for_user(self, user): - # TODO: implement this when we have user support + if user.is_anonymous: + return self.get_queryset().none() + + if user.is_superuser: + return self.get_queryset() + + # TODO: access profile + return self.get_queryset() @@ -208,7 +215,14 @@ class Meta: class AlertManager(models.Manager): def get_for_user(self, user): - # TODO: implement this when we have user support + if user.is_anonymous: + return self.get_queryset().none() + + if user.is_superuser: + return self.get_queryset() + + # TODO: access profile + return self.get_queryset() diff --git a/humitifier-server/src/hosts/views.py b/humitifier-server/src/hosts/views.py index 706805c..23b1011 100644 --- a/humitifier-server/src/hosts/views.py +++ b/humitifier-server/src/hosts/views.py @@ -1,5 +1,6 @@ import json +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.forms import Form from django.http import HttpResponse, HttpResponseRedirect @@ -11,14 +12,14 @@ SingleObjectTemplateResponseMixin from django.views.generic.edit import FormMixin -from main.views import FilteredListView +from main.views import FilteredListView, SuperuserRequiredMixin from .filters import HostFilters from .models import Host # Create your views here. -class HostsListView(FilteredListView): +class HostsListView(LoginRequiredMixin, FilteredListView): model = Host filterset_class = HostFilters paginate_by = 50 @@ -49,7 +50,7 @@ def get_queryset(self): return filtered_qs.distinct() -class HostDetailView(TemplateView): +class HostDetailView(LoginRequiredMixin, TemplateView): template_name = 'hosts/detail.html' LATEST_KEY = 'latest' @@ -88,7 +89,7 @@ def get_context_data(self, **kwargs): return context -class HostsRawDownloadView(View): +class HostsRawDownloadView(LoginRequiredMixin, View): def get(self, request, fqdn): host = Host.objects.get_for_user(request.user).get(fqdn=fqdn) @@ -113,6 +114,8 @@ def get(self, request, fqdn): class ArchiveHostView( + LoginRequiredMixin, + SuperuserRequiredMixin, SingleObjectTemplateResponseMixin, FormMixin, BaseDetailView @@ -154,14 +157,14 @@ def form_valid(self, form): return HttpResponseRedirect(success_url) -class ExportView(TemplateView): +class ExportView(LoginRequiredMixin, TemplateView): template_name = 'main/not_implemented.html' -class TasksView(TemplateView): +class TasksView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): template_name = 'main/not_implemented.html' -class ScanProfilesView(TemplateView): +class ScanProfilesView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): template_name = 'main/not_implemented.html' -class DataSourcesView(TemplateView): +class DataSourcesView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): template_name = 'main/not_implemented.html' \ No newline at end of file diff --git a/humitifier-server/src/humitifier_server/settings.py b/humitifier-server/src/humitifier_server/settings.py index 8aa9dc4..470470d 100644 --- a/humitifier-server/src/humitifier_server/settings.py +++ b/humitifier-server/src/humitifier_server/settings.py @@ -121,12 +121,13 @@ } } -# OpenID Connect - +# Authentication +LOGIN_URL = reverse_lazy("main:login") LOGIN_REDIRECT_URL = reverse_lazy("main:home") LOGOUT_REDIRECT_URL = reverse_lazy("main:home") +## OpenID Connect if env.get_boolean("DJANGO_OIDC_ENABLED", default=False): try: diff --git a/humitifier-server/src/main/context_processors.py b/humitifier-server/src/main/context_processors.py index a7867de..bcdcdaf 100644 --- a/humitifier-server/src/main/context_processors.py +++ b/humitifier-server/src/main/context_processors.py @@ -18,6 +18,8 @@ def layout_context(request): wild_wasteland = settings.DEBUG # TODO: user setting + oidc_enabled = hasattr(settings, "OIDC_RP_CLIENT_ID") + if wild_wasteland: jokes = [ "Performs best on a 386", @@ -40,6 +42,7 @@ def layout_context(request): "num_info_alerts": all_alerts.filter(level=AlertLevel.INFO).count(), "num_warning_alerts": all_alerts.filter(level=AlertLevel.WARNING).count(), "num_critical_alerts": all_alerts.filter(level=AlertLevel.CRITICAL).count(), + "oidc_enabled": oidc_enabled, "wild_wasteland": wild_wasteland, "tag_line": tag_line, } diff --git a/humitifier-server/src/main/menus.py b/humitifier-server/src/main/menus.py index f050b55..0cf2237 100644 --- a/humitifier-server/src/main/menus.py +++ b/humitifier-server/src/main/menus.py @@ -8,6 +8,7 @@ reverse("main:dashboard"), weight=1, icon="icons/dashboard.html", + check=lambda request: request.user.is_authenticated, ) ) @@ -19,6 +20,7 @@ weight=20, icon="icons/users.html", separator=True, + check=lambda request: request.user.is_superuser, ) ) @@ -28,6 +30,7 @@ "Access profiles", reverse("main:access_profiles"), weight=21, + check=lambda request: request.user.is_superuser, icon="icons/shield.html", ) ) @@ -38,6 +41,7 @@ "OAuth2 Applications", reverse("main:oauth_applications"), weight=21, + check=lambda request: request.user.is_superuser, icon="icons/api.html", ) ) diff --git a/humitifier-server/src/main/static/main/css/tailwind.css b/humitifier-server/src/main/static/main/css/tailwind.css index ce746a2..7a51916 100644 --- a/humitifier-server/src/main/static/main/css/tailwind.css +++ b/humitifier-server/src/main/static/main/css/tailwind.css @@ -693,6 +693,25 @@ video { background-color: rgb(63 63 70 / var(--tw-bg-opacity)); } +.btn-primary { + --tw-bg-opacity: 1; + background-color: rgb(255 205 0 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.btn-primary:hover { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.text-orange { + --tw-text-opacity: 1; + color: rgb(249 115 22 / var(--tw-text-opacity)); +} + .visible { visibility: visible; } @@ -754,6 +773,11 @@ video { margin-bottom: 0.5rem; } +.mx-auto { + margin-left: auto; + margin-right: auto; +} + .mb-0 { margin-bottom: 0px; } @@ -790,12 +814,32 @@ video { margin-top: 1rem; } +.mt-6 { + margin-top: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + .mt-3 { margin-top: 0.75rem; } -.mt-6 { - margin-top: 1.5rem; +.mb-7 { + margin-bottom: 1.75rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-20 { + margin-bottom: 5rem; +} + +.mb-10 { + margin-bottom: 2.5rem; } .block { @@ -848,6 +892,18 @@ video { height: 100vh; } +.h-dvh { + height: 100dvh; +} + +.h-24 { + height: 6rem; +} + +.h-px { + height: 1px; +} + .max-h-\[50vh\] { max-height: 50vh; } @@ -876,6 +932,14 @@ video { width: 100%; } +.w-dvw { + width: 100dvw; +} + +.w-24 { + width: 6rem; +} + .flex-shrink { flex-shrink: 1; } @@ -968,6 +1032,10 @@ video { justify-content: flex-end; } +.justify-center { + justify-content: center; +} + .justify-between { justify-content: space-between; } @@ -984,6 +1052,14 @@ video { gap: 1rem; } +.gap-5 { + gap: 1.25rem; +} + +.gap-8 { + gap: 2rem; +} + .gap-x-2 { -moz-column-gap: 0.5rem; column-gap: 0.5rem; @@ -1119,6 +1195,21 @@ video { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(161 161 170 / var(--tw-bg-opacity)); +} + +.bg-gray-600 { + --tw-bg-opacity: 1; + background-color: rgb(82 82 91 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 250 250 / var(--tw-bg-opacity)); +} + .stroke-2 { stroke-width: 2; } @@ -1139,6 +1230,10 @@ video { padding: 1.25rem; } +.p-8 { + padding: 2rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1247,6 +1342,11 @@ video { line-height: 1.75rem; } +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + .font-bold { font-weight: 700; } @@ -1319,6 +1419,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(82 82 91 / var(--tw-text-opacity)); +} + .underline { text-decoration-line: underline; } @@ -1400,6 +1505,20 @@ html:not(.dark) .light\:btn-primary:hover { background-color: rgb(63 63 70 / var(--tw-bg-opacity)); } +.dark\:btn-primary:where(.dark, .dark *) { + --tw-bg-opacity: 1; + background-color: rgb(255 205 0 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.dark\:btn-primary:where(.dark, .dark *):hover { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + .last\:mb-0:last-child { margin-bottom: 0px; } @@ -1436,6 +1555,22 @@ html:not(.dark) .light\:btn-primary:hover { width: 16rem; } + .md\:w-96 { + width: 24rem; + } + + .md\:w-\[100rem\] { + width: 100rem; + } + + .md\:w-\[70rem\] { + width: 70rem; + } + + .md\:w-\[60rem\] { + width: 60rem; + } + .md\:translate-x-0 { --tw-translate-x: 0px; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); @@ -1444,6 +1579,10 @@ html:not(.dark) .light\:btn-primary:hover { .md\:grid-cols-\[1fr_2fr\] { grid-template-columns: 1fr 2fr; } + + .md\:flex-row { + flex-direction: row; + } } @media (min-width: 1024px) { @@ -1451,10 +1590,34 @@ html:not(.dark) .light\:btn-primary:hover { display: table-cell; } + .lg\:h-auto { + height: auto; + } + + .lg\:w-96 { + width: 24rem; + } + + .lg\:w-\[60rem\] { + width: 60rem; + } + .lg\:columns-2 { -moz-columns: 2; columns: 2; } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:justify-center { + justify-content: center; + } + + .lg\:justify-between { + justify-content: space-between; + } } @media (min-width: 1280px) { @@ -1569,6 +1732,21 @@ html:not(.dark) .light\:btn-primary:hover { background-color: rgb(153 27 27 / var(--tw-bg-opacity)); } +.dark\:bg-gray-400:where(.dark, .dark *) { + --tw-bg-opacity: 1; + background-color: rgb(161 161 170 / var(--tw-bg-opacity)); +} + +.dark\:bg-gray-300:where(.dark, .dark *) { + --tw-bg-opacity: 1; + background-color: rgb(212 212 216 / var(--tw-bg-opacity)); +} + +.dark\:bg-gray-500:where(.dark, .dark *) { + --tw-bg-opacity: 1; + background-color: rgb(113 113 122 / var(--tw-bg-opacity)); +} + .dark\:text-blue-300:where(.dark, .dark *) { --tw-text-opacity: 1; color: rgb(147 197 253 / var(--tw-text-opacity)); diff --git a/humitifier-server/src/main/templates/base/base_html_template.html b/humitifier-server/src/main/templates/base/base_html_template.html new file mode 100644 index 0000000..f7196da --- /dev/null +++ b/humitifier-server/src/main/templates/base/base_html_template.html @@ -0,0 +1,33 @@ +{% load static %} + + + + + + + {% block page_title %}Humitifier{% endblock %} + + + {% if debug %} + + {% endif %} + {% block head %}{% endblock %} + + + +{% block body %}{% endblock %} + + \ No newline at end of file diff --git a/humitifier-server/src/main/templates/base/base_page_template.html b/humitifier-server/src/main/templates/base/base_page_template.html index c4a75a7..ad42c73 100644 --- a/humitifier-server/src/main/templates/base/base_page_template.html +++ b/humitifier-server/src/main/templates/base/base_page_template.html @@ -1,42 +1,14 @@ +{% extends 'base/base_html_template.html' %} {% load static %} - - - - - - {% block page_title %}Humitifier{% endblock %} - - - {% if debug %} - - {% endif %} - {% block head %}{% endblock %} - - - +{% block body %} + {% include 'base/page_parts/sidebar.html' %} -{% include 'base/page_parts/sidebar.html' %} +
+ {% include 'base/page_parts/header.html' %} -
- {% include 'base/page_parts/header.html' %} + {% block content %} + {% endblock %} - {% block content %} - {% endblock %} - -
- - \ No newline at end of file +
+{% endblock %} \ No newline at end of file diff --git a/humitifier-server/src/main/templates/registration/login.html b/humitifier-server/src/main/templates/registration/login.html new file mode 100644 index 0000000..39c366f --- /dev/null +++ b/humitifier-server/src/main/templates/registration/login.html @@ -0,0 +1,77 @@ +{% extends 'base/base_html_template.html' %} +{% load static %} + +{% block page_title %}Login {{ block.super }}{% endblock %} + + +{% block body %} +
+ +
+
+ UU Logo +
+
+ Humitifier +
+
+ Humanities-IT Services CMDB +
+
+
+
+

Login

+ + {% if user.is_authenticated %} +

+ Your account doesn't have access to this page. To proceed, + please login with an account that has access. +

+ {% endif %} + + {% if form.errors %} +

Your username and password didn't match. Please try again.

+ {% endif %} + +
+ {% csrf_token %} +
+ +
+ {{ form.username }} +
+
+
+ +
+ {{ form.password }} +
+
+ + +
+ + {% if layout.oidc_enabled %} +
+
+
or
+
+
+ + Login with Solis-ID + + {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/humitifier-server/src/main/urls.py b/humitifier-server/src/main/urls.py index 66cb867..fca87e0 100644 --- a/humitifier-server/src/main/urls.py +++ b/humitifier-server/src/main/urls.py @@ -1,3 +1,4 @@ +from django.contrib.auth.views import LoginView from django.urls import path from .views import AccessProfilesView, DashboardView, HomeRedirectView, \ @@ -12,4 +13,6 @@ path("users/", UsersView.as_view(), name="users"), path("access-profiles/", AccessProfilesView.as_view(), name="access_profiles"), path("oauth-applications/", OAuthApplicationsView.as_view(), name="oauth_applications"), + + path("login", LoginView.as_view(), name="login"), ] diff --git a/humitifier-server/src/main/views.py b/humitifier-server/src/main/views.py index f7c247b..cd647a0 100644 --- a/humitifier-server/src/main/views.py +++ b/humitifier-server/src/main/views.py @@ -1,9 +1,48 @@ +from urllib.parse import urlparse + +from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin +from django.contrib.auth.views import redirect_to_login from django.db.models import Count +from django.shortcuts import resolve_url from django.urls import reverse from django.views.generic import ListView, RedirectView, TemplateView from hosts.filters import AlertFilters -from hosts.models import Alert, AlertType, Host +from hosts.models import Alert, Host + +### +### Mixins +### + +class SuperuserRequiredMixin(AccessMixin): + """ + Require users to be superusers to access the view. + """ + + def dispatch(self, request, *args, **kwargs): + """Call the appropriate handler if the user is a superuser""" + if not request.user.is_superuser: + return self.handle_no_permission() + + return super().dispatch(request, *args, **kwargs) + + def handle_no_permission(self): + """Redirect to the login page if the user is not a superuser""" + path = self.request.build_absolute_uri() + resolved_login_url = resolve_url(self.get_login_url()) + # If the login url is the same scheme and net location then use the + # path as the "next" url. + login_scheme, login_netloc = urlparse(resolved_login_url)[:2] + current_scheme, current_netloc = urlparse(path)[:2] + if (not login_scheme or login_scheme == current_scheme) and ( + not login_netloc or login_netloc == current_netloc + ): + path = self.request.get_full_path() + return redirect_to_login( + path, + resolved_login_url, + self.get_redirect_field_name(), + ) ### @@ -70,14 +109,14 @@ def get_ordering_fields(self): ### Page views ### -class HomeRedirectView(RedirectView): +class HomeRedirectView(LoginRequiredMixin, RedirectView): def get_redirect_url(self, *args, **kwargs): # TODO: user preferences return reverse('hosts:list') -class DashboardView(FilteredListView): +class DashboardView(LoginRequiredMixin, FilteredListView): model = Alert filterset_class = AlertFilters paginate_by = 20 @@ -126,11 +165,19 @@ def get_context_data(self, **kwargs): return context -class UsersView(TemplateView): +class UsersView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): template_name = 'main/not_implemented.html' -class AccessProfilesView(TemplateView): +class AccessProfilesView( + LoginRequiredMixin, + SuperuserRequiredMixin, + TemplateView +): template_name = 'main/not_implemented.html' -class OAuthApplicationsView(TemplateView): +class OAuthApplicationsView( + LoginRequiredMixin, + SuperuserRequiredMixin, + TemplateView +): template_name = 'main/not_implemented.html' \ No newline at end of file From 503e32db46ea6881f0fb925bb1e5f910111757c5 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 16:47:56 +0100 Subject: [PATCH 06/30] feat: implement access profiles and some user attributes --- humitifier-server/src/hosts/models.py | 18 +++-- .../src/main/context_processors.py | 4 +- ...ccessprofile_user_default_home_and_more.py | 66 +++++++++++++++++++ humitifier-server/src/main/models.py | 62 ++++++++++++++++- 4 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 humitifier-server/src/main/migrations/0002_accessprofile_user_default_home_and_more.py diff --git a/humitifier-server/src/hosts/models.py b/humitifier-server/src/hosts/models.py index d7eea56..005277e 100644 --- a/humitifier-server/src/hosts/models.py +++ b/humitifier-server/src/hosts/models.py @@ -47,9 +47,14 @@ def get_for_user(self, user): if user.is_superuser: return self.get_queryset() - # TODO: access profile + if user.access_profile: + return self.get_queryset().filter( + department__in=user.access_profile.departments_for_filter + ) - return self.get_queryset() + # When a non-superuser has no access profile, THEY GET NOTHING + # They lose! Good day sir! + return self.get_queryset().none() class Host(models.Model): @@ -221,9 +226,14 @@ def get_for_user(self, user): if user.is_superuser: return self.get_queryset() - # TODO: access profile + if user.access_profile: + return self.get_queryset().filter( + host__department__in=user.access_profile.departments_for_filter + ) - return self.get_queryset() + # When a non-superuser has no access profile, THEY GET NOTHING + # They lose! Good day sir! + return self.get_queryset().none() class Alert(models.Model): diff --git a/humitifier-server/src/main/context_processors.py b/humitifier-server/src/main/context_processors.py index bcdcdaf..bc2b557 100644 --- a/humitifier-server/src/main/context_processors.py +++ b/humitifier-server/src/main/context_processors.py @@ -16,7 +16,9 @@ def layout_context(request): tag_line = "HumIT CMDB" - wild_wasteland = settings.DEBUG # TODO: user setting + wild_wasteland = False + if user.is_authenticated: + wild_wasteland = user.wild_wasteland_mode oidc_enabled = hasattr(settings, "OIDC_RP_CLIENT_ID") diff --git a/humitifier-server/src/main/migrations/0002_accessprofile_user_default_home_and_more.py b/humitifier-server/src/main/migrations/0002_accessprofile_user_default_home_and_more.py new file mode 100644 index 0000000..e04b2bd --- /dev/null +++ b/humitifier-server/src/main/migrations/0002_accessprofile_user_default_home_and_more.py @@ -0,0 +1,66 @@ +# Generated by Django 5.1.2 on 2024-11-04 15:34 + +import django.contrib.postgres.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("main", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="AccessProfile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("description", models.TextField(blank=True)), + ( + "departments", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=200), size=None + ), + ), + ], + ), + migrations.AddField( + model_name="user", + name="default_home", + field=models.CharField( + choices=[("dashboard", "Dashboard"), ("hosts", "Hosts")], + default="hosts", + max_length=20, + ), + ), + migrations.AddField( + model_name="user", + name="is_local_account", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="user", + name="wild_wasteland_mode", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="access_profile", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="main.accessprofile", + ), + ), + ] diff --git a/humitifier-server/src/main/models.py b/humitifier-server/src/main/models.py index c8e51ca..73d8e76 100644 --- a/humitifier-server/src/main/models.py +++ b/humitifier-server/src/main/models.py @@ -1,7 +1,67 @@ from django.contrib.auth.models import AbstractUser +from django.contrib.postgres.fields import ArrayField from django.db import models # Create your models here. +class HomeOptions(models.TextChoices): + DASHBOARD = 'dashboard', 'Dashboard' + HOSTS = 'hosts', 'Hosts' + class User(AbstractUser): - pass \ No newline at end of file + + is_local_account = models.BooleanField(default=True) + + wild_wasteland_mode = models.BooleanField(default=False) + + default_home = models.CharField( + max_length=20, + choices=HomeOptions.choices, + default=HomeOptions.HOSTS + ) + + access_profile = models.ForeignKey( + 'AccessProfile', + blank=True, + null=True, + on_delete=models.SET_NULL + ) + + +class AccessProfile(models.Model): + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + + departments = ArrayField( + models.CharField(max_length=200), + ) + + @property + def departments_for_filter(self): + departments = [] + + for department in self.departments: + departments.append(department) + + # Explanation for the following: the department field on hosts + # is generated from JSON data. For some reason, this adds + # quotes around the department name. + # The following code is some safeguarding to make sure that + # the filter works as expected. (As 'self.departments' is human + # input) + + # If we have a department with quotes, we need to add an option + # without quotes as well, just to be sure. + if department.endswith('"'): + departments.append(department[1:-1]) + # And the other way around too + else: + departments.append(f'"{department}"') + + return departments + + def __str__(self): + return self.name + + def __repr__(self): + return f"" \ No newline at end of file From b746a96c5aa25da8cca72f890b42a9aa5b652be7 Mon Sep 17 00:00:00 2001 From: "Mees, T.D. (Ty)" Date: Mon, 4 Nov 2024 17:16:03 +0100 Subject: [PATCH 07/30] feat: implement favicon --- .../src/main/static/main/img/favicon.ico | Bin 15406 -> 15086 bytes .../main/img/wild_wasteland_favicon.ico | Bin 0 -> 15406 bytes .../templates/base/base_html_template.html | 7 +++++++ 3 files changed, 7 insertions(+) create mode 100644 humitifier-server/src/main/static/main/img/wild_wasteland_favicon.ico diff --git a/humitifier-server/src/main/static/main/img/favicon.ico b/humitifier-server/src/main/static/main/img/favicon.ico index 807024cdb71a5e53c69d99101d3260dd6ecaed0a..6f2db548d127f860bf45415a3d1b643f1d7b6f2c 100644 GIT binary patch literal 15086 zcmeHOcYIdGwmmx)NeCSwAcW9EPau>~Lx+T7Kzc_y0i+0s0@4I&h9Y9ag7l^!TtUQ( z1-*(5>-8#L5kV190SWnL=B+i81EG2GdGCAgkN5r9%y-Ur&di>fJ$tXcX5x9`1$+7O zc_?GNp&_1^=y_gDjJsZk`%mCrjT+h47u1U2aR{Kq;A^_QYrCA zsZ?i|lqg?Ux{eF=>k-e3k}63VQls%#QZxCyH0f-l(?~7dCTQs~NlV&jEluyx(l|{^ z*%~LMX44PlmWbMy-cM{jQYs|;RcbW3D6RWx>2t4^iH~WS_o9{u|DxsI=d=u&r={%x zjeZ#D`khp7v`MNrE&yGMO0m*&(DskgqL;Dl&D^48?N?fMo;Kc#XN+t&}PzQ6cwJ+#|a{mG$!=mw9yq`V#I z$MVqp#?Q)gNdN{gJsZ zXv`bt0rQjC>bO)*x=&)_V&t~+(=mU?q*Wgyz3$QWxsCtS)YZJ<*5@)avIOL;q}Zkx|w1DVpihmp>(nVyqj3sarW&3kBRbi4NA z>KrZFIQ3*g?ZocB{jii;ttv=N<-xGoH!(KfOPw}KlDpb>FuzLbwfj}#n;ph{Z;?X9 z`*}^fhvj_V*3qz2EbOTZY+=0@Q~AZnM(xiQuGhkN6>1;AI-uPTk@_hOVDCX!zw2Ma z7J^~l)xDxIZK4~tdA4eDn=M}XT7#r~d>!b2&HB~TJnuqY&pQ(CdHaJQsC+sF6yFs6qsZ#yuzaTLX!!+gk3p z>to3k@ieYuA-VrxllF1E7b@+C-Y!LMdmrDoN`YHf!2cO3Q1lyIzlSvdUy-Qsu#qlf zjnq&52=`ZEUiM47f#>l3amce$tND8PT$;m8fAC%;BvH!MJSex1G!mI_Gp<)-?J4*O zjqgUf-|gZ6zmIxI!RKfR3N|u$u95Ny7x2tx_`oyv{XF>}yxGR#VBL+5L(yEAxq ziiCttl)CMj{VP4tMrX8l0rBW7yte|jeipvpSU=9TW^B^%%UW9Y(lTzP#@g4i;3W;) z)G}};eA0M~Ly!^rx{kvQ>NtkBUzd6*2Jd_g`>b~(9q?>*$S8@e`UZ5*Ld?y?wSn)` zGUs_M#bUMehTj|Y5Ph|l2X<=sB*d^+G;P6_oQG!JwM<*5B~N~gPagQpF-FoxDU8); zsINC9&(%60{uGu{aZ^Mj9ly_&=vX5YAJ@`hn3nL|S_aM5GU-Xgw{}_{eoI?hSc&q^ zIZ77p(bD%mB@gT%CO}MhO~cn|DO^I!nB|D~{b28m zRcFyQ=gi2hrL^3)*2R}{H4!uR^ShFazcT(SdG1RsE8kM`$lFR*y`yF93MKP*X&L;0 zmdf=Jub>a-gZ2^{7J&c%&5lL0?uR8`;pm(?puGkX5w#KH`aS9~cAQIA)L4J8ul0<- zuCHwKna^x2mmQ~!>^N=Y`IB0<|ET4O52y>`(ZUX=bXVh z;o7|9^YKhI*vzB2*Awz4bmN-VvJiGP0`ak7CuQ-&)1PSBbX3cWzzjPP_bFF@f9ew@ z&wfE$RMrlrZ{YgSh#@lk#$Sz#(`XyT*luv)^=4i#6TmxG27Sa-t*VR|AT>x0l zNV`ElNSpq3I0yKxr$m=I23x);{ibV~`mB-}8{L@QaTo1f$@(L}ZD@yWZ#rs1(jQm3 z+uWz}B#cnuo#(3HIlzR#b(h9v=>a9ZrYPwR+(Nr0))>D^VNL?@m9I#l=)D*N*S_af z#h5U>z6&wX2lfMB(|sb=-F7Xj4+Hm|K>ZkR^gC$Qc3mcHf~xJU)3v?LdSHVsDtX}4 zfJbae{b&=^cO`6rYp7KpZT(Xy?9lV*%O*g&Lt=rAqR`e(lnQf{WwCIp-da+J08;{! z4V|y7trKh6_RpH&sT*|VkVU$lx6h1Bepi3@_j7vKoJ9fesZU+|)L|WTx%W9`$1`n| zMoiGaPsEs6@I~JMBYzE_F$+GX0C4#>_}Rl4o0C|pS_a(j*4)??4zE45UtR9MYrj&^ zLOm+^J$>}c^V%3~hRj-cxqa$MtUPDClDpA=+R)H>^bzzM#OoJeo8JKYPsA7qq#C3Z zgg)OOzPhzfA2((h*3{qJ`pWLVOj)nf>MjX^q;^ z@K2W`FlVUV7{0Io;+Amy1CW`JA&_gm}S=*Zr+nl*sSs$9z zK}*Sshv186qwNH1|H<9%v%X^Zf{)?<&Rc!yI~h}G1N146zN=-+_m}L8xZT@w{F1=` zT=oa_0gu0r_^?9*3t{~gcJ@c#O@H2X9OCDQ6OuaE*?-{@EfC|Mgbx}Folaw27#mk+ z!}jU-Qtnj1?QRWjLC)llF~>Ql@czbcwLJT|zO?oh?{n)oCeGmo&pU`1xEnrUmn1YR zV*BrTxzKJ?_~?P~Kd-{(-5eyg(!c^PM_775%diKPAcql-bIx}89l4b}_YW7FMlL}- zz-5A|zkF zv6gnb`Z^;Qr9UMHK-`nuRSD;q@t$+UcImU3FNftah@S>IdcaHWrn+|PX4oaFNI6LVVY9TVeFkJ42TCxJhR6}VQBOWN2q zda078UAf-Bg{^IZ|GS9%^e431>Drbr-vOS9M$X+I?=^;`!uH=qJU8$W#FB3Qi37Qg z=+7eaF}5gh1{z!g`VC*fF)LP@>mM;MZV&321Pt;c?0uxSiOQQyz02hIPv}OIP^;&p(W3pOV{0ox^&bkL!kz zYjRt^&8tg18;tiyL+5$;z8qsX5xM$t=yC|xuOO~wfhTbJ2XS~p^B-}46S$Tm$WwOU z`vQ!^Bx%#{8DPSeSJ!_t*Qg^H!o0FN=58U@Cu8SA_>Xk2=j30bY-+Yi^J;M0&r|G5w92?lnL2QD9l@3mlGtGtxKr{ikZJ2!d4`185q zo18`a#MZ50Cp}@C0}#)P|G8Fg{I=vRSQPno!=SKS9YTVnGv=g?By~tY`+vBvhMxozem&Ae97wTY&gnAU*wgNPS{|M4m7ivR!r$`Qc)5-SBkia}D4V-5pGp8eiI|Wi!J+ge#C&}qnZyVl;zsTGnr7X)b*5V7IOElA zhg>zC{Kb8c(vV1S!o7jTlfXyZ@;})Nw4EDL9DKqi_gzdU=3faeBfgot<})3J0>iCCAH(_HJ&Qkc_s2idvPUC~&zE?aYyyHj|HZ6bsJz1Wx0?- z(X4kPgn8=*%N_NZTK|t~4~!HGEcL$SaC7I$LS3|%(U6~EE6n>8_*B@*oXPH4{|SJVPa5Xf6iC>q->2Hmkgb~oqu zAg*sZ`r_GqkeZn11BgWid}toB9}1=Ic0FDMsBEN>v3`<%JN#w%gH%1PtADMai6sN5epq( zM&6S8l6zu~7+clX-B=!(>x0~B_$}<`en?wL4ahb5Sx(!Mu&^?CrW3^RNR0jLKlS1m zHoXJ-hPXcT03S(yl6gFJ%Pr! zzG>&=3@5KvQW&;dr>(oD4W|wc;5Q==A5_ShmE{XFUJclC(cXYOcF5UI+W`SQqlNve z+0@xO*ED$`&IRYeZ!@Z(vHTu6Kk_GT|Du9F<~d8eLtK&$yZj#9@~_rMkl&?!a80t# z&~V3NzwndeF=!v7!NpnrYfnI)`@7TI=OuTXa-59Mlad!%{4(YN^E4FxK0ev;68yc> zh|9KFs5o>$bmZU#q+j6k8AeuaF4NqK9~50dF@K@Bi!SFJ;J8Rm2qC9V_6UP z<(=(v?G1S#0RN~QM+@%$Y7QyqHSL{z){`G8Z~_6h_Yd7u2UBCZ{xL7Ja|M9)_E9j@>5M zGI{Wvy58_Rp4;$^vpb%3;22d(qps4nr9GFYQ+erV?@c zG1!U0*@7RCdwh=dl4WC&Z-?|b@y)mw~*guUd|G+#}?)3`F5f zi9&wjY_DVm=kuZpkTZ8&8}*}|lh^mh+Shga3FEE)NPG9JR^GF)F-;eH8A`>G({nb<*tP)sTJv`)?3>ZYf_tk*`-sf89b!zztdm&#*r$n6 zI6I*1{s{SIjtyfgImjtbXLGPq1HhDlEA*jm@PEPGrl|1vu7SvsDS?8aja6jnPJyuM zX@Q`g@Sj)jGqDeg_XU6v0@7iy zXt$~5ZPcAAtXCdgx4koS2rlh1)CHdh4pNMSd$Lt@F;s^skh=wkqD!TD0Z57vGjP-VS|s(YokIPTbk zyZo{GUu{2J^J~9S$aw?c?gLV~vcu%u7h}6@pL)=Li=2mk2Ot@cL68!ce5hL&3Ai^6 z;^*jY4Kt4=P9dIM@E7<6tYLp&*w;PVe!M&9>c+8CvfkZyQYn>XvMydU7c(=pR!T2f zjJ?dO_4nVWFXo&Qo6=sY1D9}rhqtzOVntCAxH6VxV2OUV#q1`m2t;`|FhMeQ@17P`OT0 zz~eaQ^gn#>;e!F}2QxN0+hJQ<(H8fK8TT8d;rtx--a|q!K)=n9o)Gdd_N)RhbrIwo zd*NHo0z+kDonElMo@5h^5{!MP>ovld4K z=Y?9_KYXFVd6S=k^S7Zd=fLSOU%l+d3PN|<5b?f^nFo zbD#OU9G2$u%;)I$ecpvWJR}U<67me5A>zIfbLmk_$cJq}e7%$hU{7!?*2rn}iRWav zHpsy^zsp>O=oi(HOOE{FU0O;;4m**3{L!1v&m(?gJ(S%2WZR0?erJalRB+GlI>Gz z+gHs2$NQPjl-=8hev!wydp=(v%0?B()Q!((B54R#`7u>QNQ8$G+*bOrBrKB z2>iwy(8C19#HpYVv8bV zzzei7`mwRgUEbJ!h_ZRr&eJ&Cg?sc7r7LqE+>J$T^v7=(We|Q3imk@@W02SE#ah1M zewdW1*gYsX`26VfQF_+QnF{+SZVi(+;W;bjNQ^72pL^W&Av}LT{)9X<$Cz9M_uiiY z7D#IE#+rNdv=8QR%+>ikV+(zK{S>D!{R}Y=`IEYB6~@*?sB>m#te?* zD(HJNd(D+2BO*S*-UG(C8|O6>=T*K$EGG}jJ$H+fZ7e2_#(T60jwSsP$B}2V$g!}$ z+zVg~=a>^a60iAudt6<|ztIP{b3ni>K^NiYP5?g!XtPn!1M~16bT50I@xIZm_Fkjg zPukq4O48%daSY@HKf$fdA`Ckj6u)a zXY&<41rBe(jr9Dz`*=SDk{iO9(HBx5eD;^ffiJ?(G9J>G;mjfOdGc+v8(#B_A91|D zUr`O~nb*FpaX*In57!l+XH3ms0^fl!9{tO+9XIL+--R$QZUx-* zvfR5S6Sg@O*Bv2a;rouFKj1XS`?~ONHONmmEJpo!KKQZsh>2bQc#mA_xRqE-L0_Yu z5s*~)v)NeRnc#i6&d5U*gIxN%og3+RGxt$X8N|y2SWk@O%%3^WW8o{8TW-Bn+;_M> z2>Ate<9xZlKf%12JS1bE+jDv!XKsGMIbQB9ExX~D6 z!TvLz!F317I!Ilt|GqM0#(!*&9&O;C{jV_M?`&|ezX4-0ub2ZNcRB|6;DQGqUa5RJ z8+*!?F0FB{2G||EfXn|le@`H%<390O;NE+2T@XSZ>c4I4H#-m6`iJ80kg6f3PeRPu zigxQlhC{vu4`sZ>Ho8^wX6DmRKQ)b$>bo;`9kJK#d}D2t`lSI3hYmQXh*xqkJy!4=5GJD+lFH|u*giH6b~vHvn{VJb-t*^yuO%Segg-@>sPV( zSgr>7$TA-cb{hUR3Z>T=C5N+==MjAI+OX^ZLW3nn+b?kj%3j+N&!6Nq*v$Pb(P6(l zl6`HT1M0N*uk)P!9rtaCzrwI3+Vku2OTV1}>%~bjTjtQ~3jNTY->!WQdg>Uw*BJd8 t1B0K25-$W9l;49uX89e0B}-K_q%TWdG-Mh}vn|YytGVFD*ABer{TCEaxmf@J literal 15406 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 Qfy3qg{`GqcoR}8)FQ4)eApigX diff --git a/humitifier-server/src/main/static/main/img/wild_wasteland_favicon.ico b/humitifier-server/src/main/static/main/img/wild_wasteland_favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..807024cdb71a5e53c69d99101d3260dd6ecaed0a GIT binary patch literal 15406 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 Qfy3qg{`GqcoR}8)FQ4)eApigX literal 0 HcmV?d00001 diff --git a/humitifier-server/src/main/templates/base/base_html_template.html b/humitifier-server/src/main/templates/base/base_html_template.html index f7196da..e61a069 100644 --- a/humitifier-server/src/main/templates/base/base_html_template.html +++ b/humitifier-server/src/main/templates/base/base_html_template.html @@ -7,6 +7,13 @@ {% block page_title %}Humitifier{% endblock %} + + {% if layout.wild_wasteland %} + + {% else %} + + {% endif %} + {% if debug %}