interloper-api 0.65.1__tar.gz → 0.67.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {interloper_api-0.65.1 → interloper_api-0.67.0}/PKG-INFO +1 -1
- {interloper_api-0.65.1 → interloper_api-0.67.0}/pyproject.toml +25 -1
- {interloper_api-0.65.1 → interloper_api-0.67.0}/pyproject.toml.orig +22 -3
- interloper_api-0.67.0/src/interloper_api/__init__.py +5 -0
- interloper_api-0.67.0/src/interloper_api/app.py +241 -0
- interloper_api-0.67.0/src/interloper_api/dependencies/__init__.py +64 -0
- interloper_api-0.67.0/src/interloper_api/dependencies/auth.py +108 -0
- interloper_api-0.67.0/src/interloper_api/dependencies/rbac.py +193 -0
- interloper_api-0.67.0/src/interloper_api/dependencies/state.py +171 -0
- interloper_api-0.67.0/src/interloper_api/notifications/__init__.py +5 -0
- interloper_api-0.65.1/src/interloper_api/email.py → interloper_api-0.67.0/src/interloper_api/notifications/invitations.py +86 -75
- interloper_api-0.67.0/src/interloper_api/routes/__init__.py +1 -0
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/admin.py +447 -189
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/agent.py +79 -18
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/auth.py +142 -25
- interloper_api-0.67.0/src/interloper_api/routes/backfills.py +207 -0
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/catalog.py +20 -1
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/components.py +328 -129
- interloper_api-0.67.0/src/interloper_api/routes/health.py +17 -0
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/oauth.py +40 -23
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/organisations.py +131 -38
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/runs.py +146 -62
- {interloper_api-0.65.1 → interloper_api-0.67.0}/src/interloper_api/routes/tokens.py +44 -9
- interloper_api-0.65.1/src/interloper_api/routes/ws.py → interloper_api-0.67.0/src/interloper_api/routes/websocket.py +72 -30
- interloper_api-0.65.1/src/interloper_api/__init__.py +0 -3
- interloper_api-0.65.1/src/interloper_api/app.py +0 -145
- interloper_api-0.65.1/src/interloper_api/components.py +0 -64
- interloper_api-0.65.1/src/interloper_api/dependencies.py +0 -449
- interloper_api-0.65.1/src/interloper_api/routes/__init__.py +0 -0
- interloper_api-0.65.1/src/interloper_api/routes/backfills.py +0 -149
- {interloper_api-0.65.1 → interloper_api-0.67.0}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "interloper-api"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.67.0"
|
|
4
4
|
description = "Interloper FastAPI routes"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -43,6 +43,7 @@ workspace = true
|
|
|
43
43
|
line-length = 120
|
|
44
44
|
|
|
45
45
|
[tool.ruff.lint]
|
|
46
|
+
preview = true
|
|
46
47
|
extend-select = [
|
|
47
48
|
"E",
|
|
48
49
|
"I",
|
|
@@ -50,6 +51,24 @@ extend-select = [
|
|
|
50
51
|
"ANN001",
|
|
51
52
|
"ANN201",
|
|
52
53
|
"ANN202",
|
|
54
|
+
"DOC",
|
|
55
|
+
"D",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.pydocstyle]
|
|
59
|
+
convention = "google"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
62
|
+
extend-immutable-calls = [
|
|
63
|
+
"fastapi.Body",
|
|
64
|
+
"fastapi.Cookie",
|
|
65
|
+
"fastapi.Depends",
|
|
66
|
+
"fastapi.File",
|
|
67
|
+
"fastapi.Form",
|
|
68
|
+
"fastapi.Header",
|
|
69
|
+
"fastapi.Path",
|
|
70
|
+
"fastapi.Query",
|
|
71
|
+
"fastapi.Security",
|
|
53
72
|
]
|
|
54
73
|
|
|
55
74
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -60,4 +79,9 @@ extend-select = [
|
|
|
60
79
|
"tests/**" = [
|
|
61
80
|
"ANN",
|
|
62
81
|
"F811",
|
|
82
|
+
"D101",
|
|
83
|
+
"D102",
|
|
84
|
+
"D103",
|
|
85
|
+
"D104",
|
|
86
|
+
"D107",
|
|
63
87
|
]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ###############
|
|
4
4
|
[project]
|
|
5
5
|
name = "interloper-api"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.67.0"
|
|
7
7
|
description = "Interloper FastAPI routes"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
|
|
@@ -42,8 +42,27 @@ interloper-agent = { workspace = true }
|
|
|
42
42
|
line-length = 120
|
|
43
43
|
|
|
44
44
|
[tool.ruff.lint]
|
|
45
|
-
|
|
45
|
+
preview = true
|
|
46
|
+
extend-select = ["E", "I", "UP", "ANN001", "ANN201", "ANN202", "DOC", "D"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.pydocstyle]
|
|
49
|
+
convention = "google"
|
|
50
|
+
|
|
51
|
+
# FastAPI's whole dependency-injection surface is calls in argument defaults;
|
|
52
|
+
# B008 exists for mutable defaults, which these are not.
|
|
53
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
54
|
+
extend-immutable-calls = [
|
|
55
|
+
"fastapi.Body",
|
|
56
|
+
"fastapi.Cookie",
|
|
57
|
+
"fastapi.Depends",
|
|
58
|
+
"fastapi.File",
|
|
59
|
+
"fastapi.Form",
|
|
60
|
+
"fastapi.Header",
|
|
61
|
+
"fastapi.Path",
|
|
62
|
+
"fastapi.Query",
|
|
63
|
+
"fastapi.Security",
|
|
64
|
+
]
|
|
46
65
|
|
|
47
66
|
[tool.ruff.lint.per-file-ignores]
|
|
48
67
|
"__init__.py" = ["F401", "F403"]
|
|
49
|
-
"tests/**" = ["ANN", "F811"]
|
|
68
|
+
"tests/**" = ["ANN", "F811", "D101", "D102", "D103", "D104", "D107"]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""FastAPI application factory for the interloper API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, FastAPI, Request
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
from fastapi.responses import JSONResponse
|
|
12
|
+
from interloper.catalog.base import Catalog
|
|
13
|
+
from interloper.errors import ComponentDriftError, NotFoundError, QuotaExceededError
|
|
14
|
+
from interloper_db import Store
|
|
15
|
+
|
|
16
|
+
from interloper_api.dependencies import (
|
|
17
|
+
set_admin_config,
|
|
18
|
+
set_auth_config,
|
|
19
|
+
set_catalog,
|
|
20
|
+
set_features,
|
|
21
|
+
set_quota_defaults,
|
|
22
|
+
set_smtp_config,
|
|
23
|
+
set_store,
|
|
24
|
+
)
|
|
25
|
+
from interloper_api.routes import (
|
|
26
|
+
admin,
|
|
27
|
+
auth,
|
|
28
|
+
backfills,
|
|
29
|
+
components,
|
|
30
|
+
health,
|
|
31
|
+
oauth,
|
|
32
|
+
organisations,
|
|
33
|
+
runs,
|
|
34
|
+
tokens,
|
|
35
|
+
websocket,
|
|
36
|
+
)
|
|
37
|
+
from interloper_api.routes import catalog as catalog_routes
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
#: Routers mounted under ``/api`` for every deployment. The agent router is
|
|
42
|
+
#: absent by design: it is optional and mounted by :func:`_mount_agent`.
|
|
43
|
+
_ROUTE_MODULES: tuple[ModuleType, ...] = (
|
|
44
|
+
auth,
|
|
45
|
+
organisations,
|
|
46
|
+
admin,
|
|
47
|
+
catalog_routes,
|
|
48
|
+
components,
|
|
49
|
+
runs,
|
|
50
|
+
backfills,
|
|
51
|
+
oauth,
|
|
52
|
+
tokens,
|
|
53
|
+
websocket,
|
|
54
|
+
health,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# -- Error handling ------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def _not_found(_request: Request, exception: NotFoundError) -> JSONResponse:
|
|
62
|
+
"""Render a missing store target as a plain 404.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
_request: The incoming request, unused.
|
|
66
|
+
exception: The raised :class:`NotFoundError`.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A 404 response carrying the exception message as ``detail``.
|
|
70
|
+
"""
|
|
71
|
+
return JSONResponse(status_code=404, content={"detail": str(exception)})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _component_drift(_request: Request, exception: ComponentDriftError) -> JSONResponse:
|
|
75
|
+
"""Render catalog drift as a conflict rather than a 500.
|
|
76
|
+
|
|
77
|
+
Hydrating or running a drifted source/asset cannot succeed until the user
|
|
78
|
+
resolves the drift, so it surfaces as a clean 409 the UI can act on.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
_request: The incoming request, unused.
|
|
82
|
+
exception: The raised :class:`ComponentDriftError`.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A 409 response carrying the exception message as ``detail``.
|
|
86
|
+
"""
|
|
87
|
+
return JSONResponse(status_code=409, content={"detail": str(exception)})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _quota_exceeded(_request: Request, exception: QuotaExceededError) -> JSONResponse:
|
|
91
|
+
"""Render store-level quota enforcement as a 429 with structured context.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
_request: The incoming request, unused.
|
|
95
|
+
exception: The raised :class:`QuotaExceededError`.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
A 429 response whose ``detail`` carries the message plus the quota
|
|
99
|
+
name, its limit, and the amount already used.
|
|
100
|
+
"""
|
|
101
|
+
return JSONResponse(
|
|
102
|
+
status_code=429,
|
|
103
|
+
content={
|
|
104
|
+
"detail": {
|
|
105
|
+
"message": str(exception),
|
|
106
|
+
"quota": exception.quota,
|
|
107
|
+
"limit": exception.limit,
|
|
108
|
+
"used": exception.used,
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
#: Framework errors that have an HTTP meaning, and the response each becomes.
|
|
115
|
+
#: Anything absent here is a bug and stays a 500.
|
|
116
|
+
_ERROR_HANDLERS: dict[type[Exception], Any] = {
|
|
117
|
+
NotFoundError: _not_found,
|
|
118
|
+
ComponentDriftError: _component_drift,
|
|
119
|
+
QuotaExceededError: _quota_exceeded,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# -- Application factory -------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_app(
|
|
127
|
+
store: Store | None = None,
|
|
128
|
+
catalog: Catalog | None = None,
|
|
129
|
+
settings: Any | None = None,
|
|
130
|
+
cors_origins: list[str] | None = None,
|
|
131
|
+
**kwargs: Any,
|
|
132
|
+
) -> FastAPI:
|
|
133
|
+
"""Create the FastAPI application with all routes.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
store: The ``Store`` instance for persistence.
|
|
137
|
+
catalog: Catalog instance.
|
|
138
|
+
settings: Full ``AppSettings``; the factory slices what it needs
|
|
139
|
+
(auth, smtp, agent) and builds the secrets-redacted snapshot for
|
|
140
|
+
the super-admin ``/admin/config`` view. The agent routes mount
|
|
141
|
+
only when enabled (or with no settings) and the ``agent`` extra
|
|
142
|
+
is installed.
|
|
143
|
+
cors_origins: Allowed CORS origins. Only needed in dev mode for direct
|
|
144
|
+
WebSocket connections that bypass the Vite proxy.
|
|
145
|
+
**kwargs: Additional kwargs forwarded to ``FastAPI()``.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The configured FastAPI application.
|
|
149
|
+
"""
|
|
150
|
+
app = FastAPI(title="Interloper API", lifespan=websocket.realtime_lifespan, **kwargs)
|
|
151
|
+
|
|
152
|
+
for error_type, handler in _ERROR_HANDLERS.items():
|
|
153
|
+
app.add_exception_handler(error_type, handler)
|
|
154
|
+
|
|
155
|
+
if cors_origins:
|
|
156
|
+
app.add_middleware(
|
|
157
|
+
CORSMiddleware,
|
|
158
|
+
allow_origins=cors_origins,
|
|
159
|
+
allow_credentials=True,
|
|
160
|
+
allow_methods=["*"],
|
|
161
|
+
allow_headers=["*"],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
api = APIRouter(prefix="/api")
|
|
165
|
+
for module in _ROUTE_MODULES:
|
|
166
|
+
api.include_router(module.router)
|
|
167
|
+
agent_available = _mount_agent(api, settings)
|
|
168
|
+
app.include_router(api)
|
|
169
|
+
|
|
170
|
+
# After mounting, so the feature flag and the config snapshot can record
|
|
171
|
+
# whether the agent actually mounted. Routes read state per request, never
|
|
172
|
+
# at mount time, so the order is safe.
|
|
173
|
+
_install_state(store, catalog, settings, agent_available=agent_available)
|
|
174
|
+
|
|
175
|
+
oauth.log_provider_status()
|
|
176
|
+
|
|
177
|
+
return app
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# -- Internals -----------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _mount_agent(api: APIRouter, settings: Any | None) -> bool:
|
|
184
|
+
"""Mount the optional agent routes, reporting whether they are available.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
api: The ``/api`` router to mount onto.
|
|
188
|
+
settings: Full ``AppSettings``, or ``None`` to mount whenever the
|
|
189
|
+
extra is installed.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True when the agent routes are mounted.
|
|
193
|
+
"""
|
|
194
|
+
if settings is not None and not settings.agent.enabled:
|
|
195
|
+
logger.info("Agent routes not mounted: disabled via settings.")
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
from interloper_api.routes import agent as agent_routes
|
|
200
|
+
except ImportError:
|
|
201
|
+
logger.warning(
|
|
202
|
+
"Agent routes not mounted: the 'agent' extra is not installed "
|
|
203
|
+
"(install interloper-api[agent]); /agent endpoints will return 404."
|
|
204
|
+
)
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
api.include_router(agent_routes.router)
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _install_state(
|
|
212
|
+
store: Store | None,
|
|
213
|
+
catalog: Catalog | None,
|
|
214
|
+
settings: Any | None,
|
|
215
|
+
*,
|
|
216
|
+
agent_available: bool,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Install the process-wide state the request dependencies read back.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
store: The ``Store`` instance, or ``None`` to leave it unset.
|
|
222
|
+
catalog: Catalog instance, or ``None`` to leave it unset.
|
|
223
|
+
settings: Full ``AppSettings``, or ``None`` to install neither the
|
|
224
|
+
settings slices nor the admin config snapshot.
|
|
225
|
+
agent_available: Whether the agent routes mounted, recorded as a
|
|
226
|
+
feature flag and in the admin config snapshot.
|
|
227
|
+
"""
|
|
228
|
+
if store:
|
|
229
|
+
set_store(store)
|
|
230
|
+
if catalog:
|
|
231
|
+
set_catalog(catalog)
|
|
232
|
+
|
|
233
|
+
set_features({"agent": agent_available})
|
|
234
|
+
|
|
235
|
+
if settings:
|
|
236
|
+
set_auth_config(settings.auth)
|
|
237
|
+
set_smtp_config(settings.smtp)
|
|
238
|
+
set_quota_defaults(settings.quota)
|
|
239
|
+
set_admin_config(
|
|
240
|
+
admin.AdminConfigResponse.from_settings(settings, features={"agent": agent_available}, catalog=catalog)
|
|
241
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared FastAPI dependencies: application state, identity, and role gates.
|
|
2
|
+
|
|
3
|
+
Three concerns, one import surface. ``state`` holds what ``create_app``
|
|
4
|
+
installs once at startup; ``auth`` resolves the caller and their active
|
|
5
|
+
organisation per request; ``rbac`` gates handlers on the caller's role.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from interloper_api.dependencies.auth import (
|
|
9
|
+
get_current_org,
|
|
10
|
+
get_current_user,
|
|
11
|
+
get_org_id,
|
|
12
|
+
get_session_context,
|
|
13
|
+
)
|
|
14
|
+
from interloper_api.dependencies.rbac import (
|
|
15
|
+
authorize_org_member,
|
|
16
|
+
load_authorized,
|
|
17
|
+
require_admin,
|
|
18
|
+
require_editor,
|
|
19
|
+
require_super_admin,
|
|
20
|
+
require_viewer,
|
|
21
|
+
)
|
|
22
|
+
from interloper_api.dependencies.state import (
|
|
23
|
+
get_admin_config,
|
|
24
|
+
get_auth_config,
|
|
25
|
+
get_catalog,
|
|
26
|
+
get_features,
|
|
27
|
+
get_quota_defaults,
|
|
28
|
+
get_smtp_config,
|
|
29
|
+
get_store,
|
|
30
|
+
set_admin_config,
|
|
31
|
+
set_auth_config,
|
|
32
|
+
set_catalog,
|
|
33
|
+
set_features,
|
|
34
|
+
set_quota_defaults,
|
|
35
|
+
set_smtp_config,
|
|
36
|
+
set_store,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"authorize_org_member",
|
|
41
|
+
"get_admin_config",
|
|
42
|
+
"get_auth_config",
|
|
43
|
+
"get_catalog",
|
|
44
|
+
"get_current_org",
|
|
45
|
+
"get_current_user",
|
|
46
|
+
"get_features",
|
|
47
|
+
"get_org_id",
|
|
48
|
+
"get_quota_defaults",
|
|
49
|
+
"get_session_context",
|
|
50
|
+
"get_smtp_config",
|
|
51
|
+
"get_store",
|
|
52
|
+
"load_authorized",
|
|
53
|
+
"require_admin",
|
|
54
|
+
"require_editor",
|
|
55
|
+
"require_super_admin",
|
|
56
|
+
"require_viewer",
|
|
57
|
+
"set_admin_config",
|
|
58
|
+
"set_auth_config",
|
|
59
|
+
"set_catalog",
|
|
60
|
+
"set_features",
|
|
61
|
+
"set_quota_defaults",
|
|
62
|
+
"set_smtp_config",
|
|
63
|
+
"set_store",
|
|
64
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Request-scoped identity: who is calling, and which organisation they are in."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fastapi import Cookie, Depends, HTTPException
|
|
8
|
+
from interloper_db import Organisation, Profile, Store
|
|
9
|
+
from interloper_db.models import AuthSession
|
|
10
|
+
|
|
11
|
+
from interloper_api.dependencies.state import get_store
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_current_user(
|
|
15
|
+
store: Store = Depends(get_store),
|
|
16
|
+
session_token: str | None = Cookie(default=None),
|
|
17
|
+
) -> Profile:
|
|
18
|
+
"""Resolve the current user from the session cookie.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
store: The Store instance.
|
|
22
|
+
session_token: Session cookie value.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The authenticated Profile.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
HTTPException: 401 if not authenticated or session invalid/expired.
|
|
29
|
+
"""
|
|
30
|
+
if not session_token:
|
|
31
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
32
|
+
|
|
33
|
+
result = store.auth.resolve_session(session_token)
|
|
34
|
+
if not result:
|
|
35
|
+
raise HTTPException(status_code=401, detail="Invalid or expired session")
|
|
36
|
+
|
|
37
|
+
profile, _ = result
|
|
38
|
+
return profile
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_session_context(
|
|
42
|
+
store: Store = Depends(get_store),
|
|
43
|
+
session_token: str | None = Cookie(default=None),
|
|
44
|
+
) -> tuple[Profile, AuthSession]:
|
|
45
|
+
"""Resolve user and session from the cookie.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
store: The Store instance.
|
|
49
|
+
session_token: Session cookie value.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
``(Profile, Session)`` tuple.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
HTTPException: 401 if not authenticated.
|
|
56
|
+
"""
|
|
57
|
+
if not session_token:
|
|
58
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
59
|
+
|
|
60
|
+
result = store.auth.resolve_session(session_token)
|
|
61
|
+
if not result:
|
|
62
|
+
raise HTTPException(status_code=401, detail="Invalid or expired session")
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_current_org(
|
|
68
|
+
store: Store = Depends(get_store),
|
|
69
|
+
session_token: str | None = Cookie(default=None),
|
|
70
|
+
) -> Organisation:
|
|
71
|
+
"""Resolve the current organisation from the session.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
store: The Store instance.
|
|
75
|
+
session_token: Session cookie value.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The active Organisation.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
HTTPException: 400 if no organisation selected, 401 if not authenticated.
|
|
82
|
+
"""
|
|
83
|
+
if not session_token:
|
|
84
|
+
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
85
|
+
|
|
86
|
+
result = store.auth.resolve_session(session_token)
|
|
87
|
+
if not result:
|
|
88
|
+
raise HTTPException(status_code=401, detail="Invalid or expired session")
|
|
89
|
+
|
|
90
|
+
_, session_row = result
|
|
91
|
+
if not session_row.organisation_id:
|
|
92
|
+
raise HTTPException(status_code=400, detail="No organisation selected")
|
|
93
|
+
|
|
94
|
+
return store.organisations.get(session_row.organisation_id)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_org_id(
|
|
98
|
+
org: Organisation = Depends(get_current_org),
|
|
99
|
+
) -> UUID:
|
|
100
|
+
"""Shorthand: return just the org UUID for route handlers.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
org: The resolved Organisation.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The organisation UUID.
|
|
107
|
+
"""
|
|
108
|
+
return org.id
|