Patch: Multi-user and OIDC polish (#612)

- Moved backend OIDC functionality to external library Authlib to help
maintainability
- Separated User settings UI into individual components, allowing for
standard settings UI decorator components to be used.
- Added full support for reverse proxy and CWA users alongside local and
OIDC
- Added mapping and syncing functionality for OIDC, CWA and reverse
proxy users
- Added per-user settings into the app-wide config system. Each config
can be declared as user-overrideable, and app-wide functionality can now
receive user-specific options via standard config calls.
- Added per-user audiobook destination config
- Updated login modal UI for simplified login, plus custom labels for
OIDC login
- Added user visibility in header dropdown
- Unified "restrict settings to admin" to use app-wide user roles.
This commit is contained in:
Alex
2026-02-12 14:38:28 +00:00
committed by GitHub
parent 2d2f54729f
commit 5bed0b20f4
64 changed files with 5775 additions and 2816 deletions
+40 -55
View File
@@ -4,7 +4,7 @@ from shelfmark.download.outputs.booklore import build_booklore_config
class TestBuildBookloreConfigWithOverrides:
"""build_booklore_config should accept per-user library/path overrides."""
"""build_booklore_config should resolve per-user library/path via config."""
BASE_SETTINGS = {
"BOOKLORE_HOST": "http://booklore:6060",
@@ -14,77 +14,62 @@ class TestBuildBookloreConfigWithOverrides:
"BOOKLORE_PATH_ID": 10,
}
def test_global_config_no_overrides(self):
def test_global_config_without_user_context(self):
config = build_booklore_config(self.BASE_SETTINGS)
assert config.library_id == 1
assert config.path_id == 10
def test_override_library_and_path(self):
overrides = {"booklore_library_id": 2, "booklore_path_id": 20}
config = build_booklore_config(self.BASE_SETTINGS, user_overrides=overrides)
def test_override_library_and_path_with_user_context(self, monkeypatch):
def fake_get(key, default=None, user_id=None):
if user_id == 7 and key == "BOOKLORE_LIBRARY_ID":
return 2
if user_id == 7 and key == "BOOKLORE_PATH_ID":
return 20
return default
monkeypatch.setattr("shelfmark.download.outputs.booklore.core_config.config.get", fake_get)
config = build_booklore_config(self.BASE_SETTINGS, user_id=7)
assert config.library_id == 2
assert config.path_id == 20
def test_override_library_only(self):
overrides = {"booklore_library_id": 3}
config = build_booklore_config(self.BASE_SETTINGS, user_overrides=overrides)
def test_override_library_only(self, monkeypatch):
def fake_get(key, default=None, user_id=None):
if user_id == 7 and key == "BOOKLORE_LIBRARY_ID":
return 3
return default
monkeypatch.setattr("shelfmark.download.outputs.booklore.core_config.config.get", fake_get)
config = build_booklore_config(self.BASE_SETTINGS, user_id=7)
assert config.library_id == 3
assert config.path_id == 10 # falls back to global
def test_override_path_only(self):
overrides = {"booklore_path_id": 30}
config = build_booklore_config(self.BASE_SETTINGS, user_overrides=overrides)
def test_override_path_only(self, monkeypatch):
def fake_get(key, default=None, user_id=None):
if user_id == 7 and key == "BOOKLORE_PATH_ID":
return 30
return default
monkeypatch.setattr("shelfmark.download.outputs.booklore.core_config.config.get", fake_get)
config = build_booklore_config(self.BASE_SETTINGS, user_id=7)
assert config.library_id == 1 # falls back to global
assert config.path_id == 30
def test_empty_overrides_uses_global(self):
config = build_booklore_config(self.BASE_SETTINGS, user_overrides={})
def test_none_user_context_uses_global(self):
config = build_booklore_config(self.BASE_SETTINGS, user_id=None)
assert config.library_id == 1
assert config.path_id == 10
def test_none_overrides_uses_global(self):
config = build_booklore_config(self.BASE_SETTINGS, user_overrides=None)
assert config.library_id == 1
assert config.path_id == 10
def test_auth_fields_remain_global(self, monkeypatch):
"""Only Booklore library/path should be resolved with user context."""
def fake_get(key, default=None, user_id=None):
if user_id == 7 and key == "BOOKLORE_LIBRARY_ID":
return 5
if user_id == 7 and key == "BOOKLORE_PATH_ID":
return 15
return default
def test_auth_fields_not_overridable(self):
"""Auth stays global - user overrides should not affect host/user/pass."""
overrides = {
"booklore_library_id": 5,
"BOOKLORE_HOST": "http://evil:6060",
"BOOKLORE_USERNAME": "hacker",
}
config = build_booklore_config(self.BASE_SETTINGS, user_overrides=overrides)
monkeypatch.setattr("shelfmark.download.outputs.booklore.core_config.config.get", fake_get)
config = build_booklore_config(self.BASE_SETTINGS, user_id=7)
assert config.base_url == "http://booklore:6060"
assert config.username == "admin"
assert config.library_id == 5
class TestOutputArgsForBooklore:
"""Download tasks should carry per-user booklore settings in output_args."""
def test_output_args_with_booklore_settings(self):
from shelfmark.core.models import DownloadTask
task = DownloadTask(
task_id="test-1",
source="direct_download",
title="Book1",
output_mode="booklore",
output_args={"booklore_library_id": 2, "booklore_path_id": 20},
user_id=1,
)
assert task.output_args["booklore_library_id"] == 2
assert task.output_args["booklore_path_id"] == 20
def test_output_args_empty_for_global_booklore(self):
from shelfmark.core.models import DownloadTask
task = DownloadTask(
task_id="test-2",
source="direct_download",
title="Book1",
output_mode="booklore",
output_args={},
)
assert task.output_args == {}