Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9afbdc91fa | ||
|
|
c0a653fa3f |
+40
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from api.console import create_console_router
|
from api.console import create_console_router
|
||||||
@@ -25,7 +27,27 @@ def create_app(
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
from fastapi import BackgroundTasks, FastAPI, HTTPException
|
from fastapi import BackgroundTasks, FastAPI, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
from starlette.types import Scope
|
||||||
|
|
||||||
|
class SpaStaticFiles(StaticFiles):
|
||||||
|
"""``StaticFiles`` variant that falls back to ``index.html`` for SPA routes.
|
||||||
|
|
||||||
|
Mirrors ``apps/cloud-api/cloud_api/app.py``'s implementation: an unknown
|
||||||
|
path like ``/ui/tasks/abc`` would otherwise 404 instead of letting the
|
||||||
|
SPA's client-side router handle it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_response(self, path: str, scope: Scope) -> Any:
|
||||||
|
try:
|
||||||
|
return await super().get_response(path, scope)
|
||||||
|
except StarletteHTTPException as exc:
|
||||||
|
if exc.status_code == 404 and path != "index.html":
|
||||||
|
return await super().get_response("index.html", scope)
|
||||||
|
raise
|
||||||
|
|
||||||
device_manager = manager or DEFAULT_MANAGER
|
device_manager = manager or DEFAULT_MANAGER
|
||||||
store = metadata_store or TaskMetadataStore()
|
store = metadata_store or TaskMetadataStore()
|
||||||
@@ -127,6 +149,24 @@ def create_app(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console_static_dir = os.environ.get("RUNTIME_CONSOLE_STATIC_DIR")
|
||||||
|
if console_static_dir:
|
||||||
|
dist_dir = Path(console_static_dir)
|
||||||
|
if not dist_dir.is_dir():
|
||||||
|
raise ValueError(
|
||||||
|
f"RUNTIME_CONSOLE_STATIC_DIR is not a directory: {dist_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/", include_in_schema=False)
|
||||||
|
async def _redirect_to_console() -> RedirectResponse:
|
||||||
|
return RedirectResponse(url="/ui/")
|
||||||
|
|
||||||
|
app.mount(
|
||||||
|
"/ui",
|
||||||
|
SpaStaticFiles(directory=str(dist_dir), html=True),
|
||||||
|
name="console",
|
||||||
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,19 @@ for local frontend development.
|
|||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Same-Origin, Single-Process Mode
|
||||||
|
|
||||||
|
For an edge/dev setup where running a separate `npm run dev` process is too heavy,
|
||||||
|
the backend can serve the built console directly from the same process:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_API_BASE_URL= npm run build
|
||||||
|
RUNTIME_CONSOLE_STATIC_DIR=$(pwd)/dist uvicorn api.rest:create_app --factory --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
`VITE_API_BASE_URL=` (empty) makes the build use relative API paths so it works
|
||||||
|
same-origin without CORS. The console is then served at `/ui/` (with `/`
|
||||||
|
redirecting there); `/console/*` remains the JSON API used by both this mode
|
||||||
|
and local `npm run dev`. Rebuild (`npm run build`) after frontend changes —
|
||||||
|
this mode does not hot-reload.
|
||||||
|
|||||||
+3
-4
@@ -7,10 +7,9 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
|
const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
|
||||||
export const API_BASE_URL = (configuredBaseUrl || "http://127.0.0.1:8000").replace(
|
export const API_BASE_URL = (
|
||||||
/\/$/,
|
configuredBaseUrl !== undefined ? configuredBaseUrl : "http://127.0.0.1:8000"
|
||||||
"",
|
).replace(/\/$/, "");
|
||||||
);
|
|
||||||
|
|
||||||
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ import vue from "@vitejs/plugin-vue";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
base: "/ui/",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,8 +91,7 @@ def enforce_user_submission_policy(
|
|||||||
if not policy.submission_enabled:
|
if not policy.submission_enabled:
|
||||||
raise TaskSubmissionPolicyError("task submission is disabled for this user")
|
raise TaskSubmissionPolicyError("task submission is disabled for this user")
|
||||||
restricted = (
|
restricted = (
|
||||||
policy.allowed_host_ids is not None
|
policy.allowed_host_ids is not None or policy.allowed_device_targets is not None
|
||||||
or policy.allowed_device_targets is not None
|
|
||||||
)
|
)
|
||||||
if not restricted:
|
if not restricted:
|
||||||
return
|
return
|
||||||
@@ -104,8 +103,12 @@ def enforce_user_submission_policy(
|
|||||||
):
|
):
|
||||||
raise TaskSubmissionPolicyError("target host is not permitted")
|
raise TaskSubmissionPolicyError("target host is not permitted")
|
||||||
if policy.allowed_device_targets is not None:
|
if policy.allowed_device_targets is not None:
|
||||||
if target_device_id is None or (
|
if (
|
||||||
target_host_id,
|
target_device_id is None
|
||||||
target_device_id,
|
or (
|
||||||
) not in policy.allowed_device_targets:
|
target_host_id,
|
||||||
|
target_device_id,
|
||||||
|
)
|
||||||
|
not in policy.allowed_device_targets
|
||||||
|
):
|
||||||
raise TaskSubmissionPolicyError("target device is not permitted")
|
raise TaskSubmissionPolicyError("target device is not permitted")
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ def upgrade() -> None:
|
|||||||
sa.Column("display_name", sa.String(), nullable=False),
|
sa.Column("display_name", sa.String(), nullable=False),
|
||||||
sa.Column("password_hash", sa.Text(), nullable=False),
|
sa.Column("password_hash", sa.Text(), nullable=False),
|
||||||
sa.Column("role", sa.String(), nullable=False),
|
sa.Column("role", sa.String(), nullable=False),
|
||||||
sa.Column("enabled", sa.Integer(), nullable=False, server_default=sa.text("1")),
|
sa.Column(
|
||||||
|
"enabled", sa.Integer(), nullable=False, server_default=sa.text("1")
|
||||||
|
),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"must_change_password",
|
"must_change_password",
|
||||||
sa.Integer(),
|
sa.Integer(),
|
||||||
@@ -103,7 +105,12 @@ def upgrade() -> None:
|
|||||||
sa.Column("action", sa.String(), nullable=False),
|
sa.Column("action", sa.String(), nullable=False),
|
||||||
sa.Column("outcome", sa.String(), nullable=False),
|
sa.Column("outcome", sa.String(), nullable=False),
|
||||||
sa.Column("correlation_id", sa.String(), nullable=True),
|
sa.Column("correlation_id", sa.String(), nullable=True),
|
||||||
sa.Column("metadata_json", sa.Text(), nullable=False, server_default=sa.text("'{}'")),
|
sa.Column(
|
||||||
|
"metadata_json",
|
||||||
|
sa.Text(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("'{}'"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
op.create_index(
|
op.create_index(
|
||||||
"ix_cloud_auth_audit_events_occurred_at",
|
"ix_cloud_auth_audit_events_occurred_at",
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ def upgrade() -> None:
|
|||||||
op.create_table(
|
op.create_table(
|
||||||
"cloud_token_reservations",
|
"cloud_token_reservations",
|
||||||
sa.Column("id", sa.String(), primary_key=True),
|
sa.Column("id", sa.String(), primary_key=True),
|
||||||
sa.Column("host_id", sa.String(), sa.ForeignKey("host_registrations.host_id", ondelete="CASCADE"), nullable=False),
|
sa.Column(
|
||||||
|
"host_id",
|
||||||
|
sa.String(),
|
||||||
|
sa.ForeignKey("host_registrations.host_id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
sa.Column("usage_day", sa.String(), nullable=False),
|
sa.Column("usage_day", sa.String(), nullable=False),
|
||||||
sa.Column("reserved_tokens", sa.Integer(), nullable=False),
|
sa.Column("reserved_tokens", sa.Integer(), nullable=False),
|
||||||
sa.Column("task_id", sa.String(), nullable=True),
|
sa.Column("task_id", sa.String(), nullable=True),
|
||||||
@@ -24,12 +29,25 @@ def upgrade() -> None:
|
|||||||
sa.Column("created_at", sa.String(), nullable=False),
|
sa.Column("created_at", sa.String(), nullable=False),
|
||||||
sa.Column("expires_at", sa.String(), nullable=False),
|
sa.Column("expires_at", sa.String(), nullable=False),
|
||||||
)
|
)
|
||||||
op.create_index("ix_cloud_token_reservations_host_day", "cloud_token_reservations", ["host_id", "usage_day"])
|
op.create_index(
|
||||||
op.create_index("ix_cloud_token_reservations_expires_at", "cloud_token_reservations", ["expires_at"])
|
"ix_cloud_token_reservations_host_day",
|
||||||
|
"cloud_token_reservations",
|
||||||
|
["host_id", "usage_day"],
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_cloud_token_reservations_expires_at",
|
||||||
|
"cloud_token_reservations",
|
||||||
|
["expires_at"],
|
||||||
|
)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"cloud_token_usage_events",
|
"cloud_token_usage_events",
|
||||||
sa.Column("id", sa.String(), primary_key=True),
|
sa.Column("id", sa.String(), primary_key=True),
|
||||||
sa.Column("host_id", sa.String(), sa.ForeignKey("host_registrations.host_id", ondelete="CASCADE"), nullable=False),
|
sa.Column(
|
||||||
|
"host_id",
|
||||||
|
sa.String(),
|
||||||
|
sa.ForeignKey("host_registrations.host_id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
sa.Column("usage_day", sa.String(), nullable=False),
|
sa.Column("usage_day", sa.String(), nullable=False),
|
||||||
sa.Column("task_id", sa.String(), nullable=True),
|
sa.Column("task_id", sa.String(), nullable=True),
|
||||||
sa.Column("attempt", sa.Integer(), nullable=True),
|
sa.Column("attempt", sa.Integer(), nullable=True),
|
||||||
@@ -40,14 +58,30 @@ def upgrade() -> None:
|
|||||||
sa.Column("total_tokens", sa.Integer(), nullable=False),
|
sa.Column("total_tokens", sa.Integer(), nullable=False),
|
||||||
sa.Column("occurred_at", sa.String(), nullable=False),
|
sa.Column("occurred_at", sa.String(), nullable=False),
|
||||||
)
|
)
|
||||||
op.create_index("ix_cloud_token_usage_events_host_day", "cloud_token_usage_events", ["host_id", "usage_day"])
|
op.create_index(
|
||||||
op.create_index("ix_cloud_token_usage_events_occurred_at", "cloud_token_usage_events", ["occurred_at"])
|
"ix_cloud_token_usage_events_host_day",
|
||||||
|
"cloud_token_usage_events",
|
||||||
|
["host_id", "usage_day"],
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_cloud_token_usage_events_occurred_at",
|
||||||
|
"cloud_token_usage_events",
|
||||||
|
["occurred_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
op.drop_index("ix_cloud_token_usage_events_occurred_at", table_name="cloud_token_usage_events")
|
op.drop_index(
|
||||||
op.drop_index("ix_cloud_token_usage_events_host_day", table_name="cloud_token_usage_events")
|
"ix_cloud_token_usage_events_occurred_at", table_name="cloud_token_usage_events"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
"ix_cloud_token_usage_events_host_day", table_name="cloud_token_usage_events"
|
||||||
|
)
|
||||||
op.drop_table("cloud_token_usage_events")
|
op.drop_table("cloud_token_usage_events")
|
||||||
op.drop_index("ix_cloud_token_reservations_expires_at", table_name="cloud_token_reservations")
|
op.drop_index(
|
||||||
op.drop_index("ix_cloud_token_reservations_host_day", table_name="cloud_token_reservations")
|
"ix_cloud_token_reservations_expires_at", table_name="cloud_token_reservations"
|
||||||
|
)
|
||||||
|
op.drop_index(
|
||||||
|
"ix_cloud_token_reservations_host_day", table_name="cloud_token_reservations"
|
||||||
|
)
|
||||||
op.drop_table("cloud_token_reservations")
|
op.drop_table("cloud_token_reservations")
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ depends_on = None
|
|||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.add_column(
|
op.add_column(
|
||||||
"host_registrations",
|
"host_registrations",
|
||||||
sa.Column("planner_transport", sa.String(), nullable=False, server_default="direct"),
|
sa.Column(
|
||||||
|
"planner_transport", sa.String(), nullable=False, server_default="direct"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -142,9 +142,7 @@ class PluginRegistry:
|
|||||||
self.register(manifest)
|
self.register(manifest)
|
||||||
result.registered.append(manifest)
|
result.registered.append(manifest)
|
||||||
except _PLUGIN_REGISTRATION_FAILURES as exc:
|
except _PLUGIN_REGISTRATION_FAILURES as exc:
|
||||||
result.errors.append(
|
result.errors.append(f"entry-point plugin {manifest.name!r}: {exc}")
|
||||||
f"entry-point plugin {manifest.name!r}: {exc}"
|
|
||||||
)
|
|
||||||
if scan_path is not None:
|
if scan_path is not None:
|
||||||
for manifest in self.discover_manifest_files(scan_path):
|
for manifest in self.discover_manifest_files(scan_path):
|
||||||
try:
|
try:
|
||||||
@@ -167,8 +165,12 @@ class PluginRegistry:
|
|||||||
for entry_point in entry_points:
|
for entry_point in entry_points:
|
||||||
try:
|
try:
|
||||||
loaded = entry_point.load()
|
loaded = entry_point.load()
|
||||||
except Exception as exc: # pragma: no cover - exercised via fake eps in tests
|
except (
|
||||||
logger.warning("entry point %r failed to load: %s", entry_point.name, exc)
|
Exception
|
||||||
|
) as exc: # pragma: no cover - exercised via fake eps in tests
|
||||||
|
logger.warning(
|
||||||
|
"entry point %r failed to load: %s", entry_point.name, exc
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
manifest = _coerce_to_manifest(loaded)
|
manifest = _coerce_to_manifest(loaded)
|
||||||
if manifest is not None:
|
if manifest is not None:
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ def create_governance_router(*, repository, auth_provider: AuthProvider) -> APIR
|
|||||||
submission_enabled=payload.submission_enabled,
|
submission_enabled=payload.submission_enabled,
|
||||||
allowed_host_ids=_unique_strings(payload.allowed_host_ids),
|
allowed_host_ids=_unique_strings(payload.allowed_host_ids),
|
||||||
allowed_device_targets=(
|
allowed_device_targets=(
|
||||||
tuple((item.host_id, item.device_id) for item in payload.allowed_device_targets)
|
tuple(
|
||||||
|
(item.host_id, item.device_id)
|
||||||
|
for item in payload.allowed_device_targets
|
||||||
|
)
|
||||||
if payload.allowed_device_targets is not None
|
if payload.allowed_device_targets is not None
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ def create_user_auth_router(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/auth/login", response_model=UserResponse)
|
@router.post("/auth/login", response_model=UserResponse)
|
||||||
def login(payload: LoginRequest, request: Request, response: Response) -> UserResponse:
|
def login(
|
||||||
|
payload: LoginRequest, request: Request, response: Response
|
||||||
|
) -> UserResponse:
|
||||||
try:
|
try:
|
||||||
result = user_auth_service.login(
|
result = user_auth_service.login(
|
||||||
username=payload.username,
|
username=payload.username,
|
||||||
@@ -142,7 +144,9 @@ def create_user_auth_router(
|
|||||||
user_id, _ = _require_session(principal)
|
user_id, _ = _require_session(principal)
|
||||||
user = user_auth_service.repository.get_user(user_id) # type: ignore[attr-defined]
|
user = user_auth_service.repository.get_user(user_id) # type: ignore[attr-defined]
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="unauthorized")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="unauthorized"
|
||||||
|
)
|
||||||
return _user_response(user)
|
return _user_response(user)
|
||||||
|
|
||||||
@router.post("/auth/logout", status_code=status.HTTP_204_NO_CONTENT)
|
@router.post("/auth/logout", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -181,7 +185,9 @@ def create_user_auth_router(
|
|||||||
detail="invalid username or password",
|
detail="invalid username or password",
|
||||||
) from exc
|
) from exc
|
||||||
except UserValidationError as exc:
|
except UserValidationError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
||||||
|
) from exc
|
||||||
_clear_cookies(response)
|
_clear_cookies(response)
|
||||||
response.status_code = status.HTTP_204_NO_CONTENT
|
response.status_code = status.HTTP_204_NO_CONTENT
|
||||||
return response
|
return response
|
||||||
@@ -200,7 +206,9 @@ def create_user_auth_router(
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
@router.post(
|
||||||
|
"/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
def create_user(payload: UserCreateRequest, request: Request) -> UserResponse:
|
def create_user(payload: UserCreateRequest, request: Request) -> UserResponse:
|
||||||
principal = _principal(request, required_scope=USERS_ADMIN_SCOPE)
|
principal = _principal(request, required_scope=USERS_ADMIN_SCOPE)
|
||||||
_require_csrf(request, principal)
|
_require_csrf(request, principal)
|
||||||
@@ -212,7 +220,9 @@ def create_user_auth_router(
|
|||||||
password=payload.password,
|
password=payload.password,
|
||||||
)
|
)
|
||||||
except (UserValidationError, UserConflictError) as exc:
|
except (UserValidationError, UserConflictError) as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
||||||
|
) from exc
|
||||||
user_auth_service.record_admin_action(
|
user_auth_service.record_admin_action(
|
||||||
actor_principal_id=principal.id,
|
actor_principal_id=principal.id,
|
||||||
target_user_id=user.id,
|
target_user_id=user.id,
|
||||||
@@ -243,11 +253,17 @@ def create_user_auth_router(
|
|||||||
updated_at=utc_now(),
|
updated_at=utc_now(),
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user not found") from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="user not found"
|
||||||
|
) from exc
|
||||||
except LastAdministratorConflictError as exc:
|
except LastAdministratorConflictError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail=str(exc)
|
||||||
|
) from exc
|
||||||
except UserValidationError as exc:
|
except UserValidationError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
||||||
|
) from exc
|
||||||
user_auth_service.record_admin_action(
|
user_auth_service.record_admin_action(
|
||||||
actor_principal_id=principal.id,
|
actor_principal_id=principal.id,
|
||||||
target_user_id=user.id,
|
target_user_id=user.id,
|
||||||
@@ -273,9 +289,13 @@ def create_user_auth_router(
|
|||||||
correlation_id=current_correlation_id(),
|
correlation_id=current_correlation_id(),
|
||||||
)
|
)
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user not found") from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="user not found"
|
||||||
|
) from exc
|
||||||
except UserValidationError as exc:
|
except UserValidationError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)
|
||||||
|
) from exc
|
||||||
return _user_response(user)
|
return _user_response(user)
|
||||||
|
|
||||||
@router.delete("/users/{user_id}/sessions", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/users/{user_id}/sessions", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
@@ -284,7 +304,9 @@ def create_user_auth_router(
|
|||||||
_require_csrf(request, principal)
|
_require_csrf(request, principal)
|
||||||
user = user_auth_service.repository.get_user(user_id) # type: ignore[attr-defined]
|
user = user_auth_service.repository.get_user(user_id) # type: ignore[attr-defined]
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user not found")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="user not found"
|
||||||
|
)
|
||||||
user_auth_service.repository.revoke_user_sessions( # type: ignore[attr-defined]
|
user_auth_service.repository.revoke_user_sessions( # type: ignore[attr-defined]
|
||||||
user_id,
|
user_id,
|
||||||
revoked_at=utc_now(),
|
revoked_at=utc_now(),
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ def utc_now() -> datetime:
|
|||||||
return datetime.now(UTC)
|
return datetime.now(UTC)
|
||||||
|
|
||||||
|
|
||||||
def csrf_matches(*, csrf_cookie: str | None, csrf_header: str | None, session: UserSession) -> bool:
|
def csrf_matches(
|
||||||
|
*, csrf_cookie: str | None, csrf_header: str | None, session: UserSession
|
||||||
|
) -> bool:
|
||||||
if not csrf_cookie or not csrf_header:
|
if not csrf_cookie or not csrf_header:
|
||||||
return False
|
return False
|
||||||
if not compare_digest(csrf_cookie, csrf_header):
|
if not compare_digest(csrf_cookie, csrf_header):
|
||||||
@@ -271,7 +273,11 @@ class UserAuthService:
|
|||||||
normalized,
|
normalized,
|
||||||
client_bucket,
|
client_bucket,
|
||||||
)
|
)
|
||||||
if throttle is not None and throttle.blocked_until and throttle.blocked_until > now:
|
if (
|
||||||
|
throttle is not None
|
||||||
|
and throttle.blocked_until
|
||||||
|
and throttle.blocked_until > now
|
||||||
|
):
|
||||||
self.password_hasher.verify_dummy(password)
|
self.password_hasher.verify_dummy(password)
|
||||||
self._audit(
|
self._audit(
|
||||||
action="login",
|
action="login",
|
||||||
|
|||||||
Reference in New Issue
Block a user