Adding audit log
This commit is contained in:
1 parent
fe6f9cca05
commit
45bc5bf8f6
18 files changed
+543
-119
No files matched your search
+181
-74
@@ -5,6 +5,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import iso_thread as iso_thread_crud
|
||||
from app.crud import manufacturer as manufacturer_crud
|
||||
from app.crud import part_type as part_type_crud
|
||||
@@ -68,10 +69,10 @@ async def create_user(
|
||||
is_superuser: bool = Form(False),
|
||||
role_ids: list[int] = Form(default=[]),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("users:write")),
|
||||
actor: User = Depends(web_require("users:write")),
|
||||
):
|
||||
if not await user_crud.get_by_email(db, email):
|
||||
await user_crud.create(
|
||||
created = await user_crud.create(
|
||||
db,
|
||||
email=email,
|
||||
password=password,
|
||||
@@ -79,6 +80,11 @@ async def create_user(
|
||||
is_superuser=is_superuser,
|
||||
role_ids=role_ids,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="User",
|
||||
resource_id=created.id,
|
||||
description=audit_crud.describe_create("User", created.id, created.email),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_users_table.html",
|
||||
@@ -129,21 +135,23 @@ async def update_user(
|
||||
is_superuser: bool = Form(False),
|
||||
role_ids: list[int] = Form(default=[]),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("users:write")),
|
||||
actor: User = Depends(web_require("users:write")),
|
||||
):
|
||||
target = await user_crud.get(db, user_id)
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
await user_crud.update(
|
||||
db,
|
||||
target,
|
||||
{
|
||||
"full_name": full_name or None,
|
||||
"password": password or None,
|
||||
"is_active": is_active,
|
||||
"is_superuser": is_superuser,
|
||||
"role_ids": role_ids,
|
||||
},
|
||||
changes = {
|
||||
"full_name": full_name or None,
|
||||
"password": password or None,
|
||||
"is_active": is_active,
|
||||
"is_superuser": is_superuser,
|
||||
"role_ids": role_ids,
|
||||
}
|
||||
await user_crud.update(db, target, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="User",
|
||||
resource_id=user_id,
|
||||
description=audit_crud.describe_update("User", user_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(request, "admin/_user_row.html", {"u": target})
|
||||
|
||||
@@ -152,12 +160,17 @@ async def update_user(
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("users:delete")),
|
||||
actor: User = Depends(web_require("users:delete")),
|
||||
):
|
||||
target = await user_crud.get(db, user_id)
|
||||
if target is not None:
|
||||
email = target.email
|
||||
await user_crud.delete(db, target)
|
||||
# Empty body — HTMX swaps the row out of existence.
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="User",
|
||||
resource_id=user_id,
|
||||
description=audit_crud.describe_delete("User", user_id, email),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -188,15 +201,20 @@ async def create_role(
|
||||
description: str = Form(""),
|
||||
permission_ids: list[int] = Form(default=[]),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("roles:write")),
|
||||
actor: User = Depends(web_require("roles:write")),
|
||||
):
|
||||
if not await role_crud.get_by_name(db, name):
|
||||
await role_crud.create(
|
||||
created = await role_crud.create(
|
||||
db,
|
||||
name=name,
|
||||
description=description or None,
|
||||
permission_ids=permission_ids,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="Role",
|
||||
resource_id=created.id,
|
||||
description=audit_crud.describe_create("Role", created.id, created.name),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_roles_table.html",
|
||||
@@ -243,12 +261,18 @@ async def update_role(
|
||||
role_id: int,
|
||||
permission_ids: list[int] = Form(default=[]),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("roles:write")),
|
||||
actor: User = Depends(web_require("roles:write")),
|
||||
):
|
||||
role = await role_crud.get(db, role_id)
|
||||
if role is None:
|
||||
raise HTTPException(status_code=404, detail="Role not found")
|
||||
await role_crud.update(db, role, {"permission_ids": permission_ids})
|
||||
changes = {"permission_ids": permission_ids}
|
||||
await role_crud.update(db, role, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="Role",
|
||||
resource_id=role_id,
|
||||
description=audit_crud.describe_update("Role", role_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(request, "admin/_role_row.html", {"r": role})
|
||||
|
||||
|
||||
@@ -256,11 +280,17 @@ async def update_role(
|
||||
async def delete_role(
|
||||
role_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("roles:write")),
|
||||
actor: User = Depends(web_require("roles:write")),
|
||||
):
|
||||
role = await role_crud.get(db, role_id)
|
||||
if role is not None:
|
||||
name = role.name
|
||||
await role_crud.delete(db, role)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="Role",
|
||||
resource_id=role_id,
|
||||
description=audit_crud.describe_delete("Role", role_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -287,14 +317,19 @@ async def create_iso_thread(
|
||||
pitch_mm: float = Form(...),
|
||||
depth_mm: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("iso_threads:write")),
|
||||
actor: User = Depends(web_require("iso_threads:write")),
|
||||
):
|
||||
await iso_thread_crud.create(
|
||||
thread = await iso_thread_crud.create(
|
||||
db,
|
||||
diameter_mm=diameter_mm,
|
||||
pitch_mm=pitch_mm,
|
||||
depth_mm=float(depth_mm) if depth_mm.strip() else None,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="IsoThread",
|
||||
resource_id=thread.id,
|
||||
description=audit_crud.describe_create("IsoThread", thread.id, f"M{thread.diameter_mm}×{thread.pitch_mm}"),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_iso_threads_table.html",
|
||||
@@ -340,19 +375,21 @@ async def update_iso_thread(
|
||||
pitch_mm: float = Form(...),
|
||||
depth_mm: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("iso_threads:write")),
|
||||
actor: User = Depends(web_require("iso_threads:write")),
|
||||
):
|
||||
thread = await iso_thread_crud.get(db, thread_id)
|
||||
if thread is None:
|
||||
raise HTTPException(status_code=404, detail="ISO thread not found")
|
||||
await iso_thread_crud.update(
|
||||
db,
|
||||
thread,
|
||||
{
|
||||
"diameter_mm": diameter_mm,
|
||||
"pitch_mm": pitch_mm,
|
||||
"depth_mm": float(depth_mm) if depth_mm.strip() else None,
|
||||
},
|
||||
changes = {
|
||||
"diameter_mm": diameter_mm,
|
||||
"pitch_mm": pitch_mm,
|
||||
"depth_mm": float(depth_mm) if depth_mm.strip() else None,
|
||||
}
|
||||
await iso_thread_crud.update(db, thread, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="IsoThread",
|
||||
resource_id=thread_id,
|
||||
description=audit_crud.describe_update("IsoThread", thread_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_iso_thread_row.html", {"t": thread}
|
||||
@@ -363,11 +400,17 @@ async def update_iso_thread(
|
||||
async def delete_iso_thread(
|
||||
thread_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("iso_threads:delete")),
|
||||
actor: User = Depends(web_require("iso_threads:delete")),
|
||||
):
|
||||
thread = await iso_thread_crud.get(db, thread_id)
|
||||
if thread is not None:
|
||||
name = f"M{thread.diameter_mm}×{thread.pitch_mm}"
|
||||
await iso_thread_crud.delete(db, thread)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="IsoThread",
|
||||
resource_id=thread_id,
|
||||
description=audit_crud.describe_delete("IsoThread", thread_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -399,15 +442,20 @@ async def create_unc_thread(
|
||||
series: UncSeries = Form(...),
|
||||
depth_in: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("unc_threads:write")),
|
||||
actor: User = Depends(web_require("unc_threads:write")),
|
||||
):
|
||||
await unc_thread_crud.create(
|
||||
thread = await unc_thread_crud.create(
|
||||
db,
|
||||
diameter_fraction=diameter_fraction,
|
||||
tpi=tpi,
|
||||
series=series,
|
||||
depth_in=float(depth_in) if depth_in.strip() else None,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="UncThread",
|
||||
resource_id=thread.id,
|
||||
description=audit_crud.describe_create("UncThread", thread.id, f"{thread.diameter_fraction}-{thread.tpi} {thread.series.value}"),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_unc_threads_table.html",
|
||||
@@ -456,20 +504,22 @@ async def update_unc_thread(
|
||||
series: UncSeries = Form(...),
|
||||
depth_in: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("unc_threads:write")),
|
||||
actor: User = Depends(web_require("unc_threads:write")),
|
||||
):
|
||||
thread = await unc_thread_crud.get(db, thread_id)
|
||||
if thread is None:
|
||||
raise HTTPException(status_code=404, detail="UNC thread not found")
|
||||
await unc_thread_crud.update(
|
||||
db,
|
||||
thread,
|
||||
{
|
||||
"diameter_fraction": diameter_fraction,
|
||||
"tpi": tpi,
|
||||
"series": series,
|
||||
"depth_in": float(depth_in) if depth_in.strip() else None,
|
||||
},
|
||||
changes = {
|
||||
"diameter_fraction": diameter_fraction,
|
||||
"tpi": tpi,
|
||||
"series": series,
|
||||
"depth_in": float(depth_in) if depth_in.strip() else None,
|
||||
}
|
||||
await unc_thread_crud.update(db, thread, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="UncThread",
|
||||
resource_id=thread_id,
|
||||
description=audit_crud.describe_update("UncThread", thread_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_unc_thread_row.html", {"t": thread}
|
||||
@@ -480,11 +530,17 @@ async def update_unc_thread(
|
||||
async def delete_unc_thread(
|
||||
thread_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("unc_threads:delete")),
|
||||
actor: User = Depends(web_require("unc_threads:delete")),
|
||||
):
|
||||
thread = await unc_thread_crud.get(db, thread_id)
|
||||
if thread is not None:
|
||||
name = f"{thread.diameter_fraction}-{thread.tpi} {thread.series.value}"
|
||||
await unc_thread_crud.delete(db, thread)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="UncThread",
|
||||
resource_id=thread_id,
|
||||
description=audit_crud.describe_delete("UncThread", thread_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -511,14 +567,19 @@ async def create_manufacturer(
|
||||
url: str = Form(...),
|
||||
logo_url: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("manufacturers:write")),
|
||||
actor: User = Depends(web_require("manufacturers:write")),
|
||||
):
|
||||
await manufacturer_crud.create(
|
||||
created = await manufacturer_crud.create(
|
||||
db,
|
||||
name=name,
|
||||
url=url,
|
||||
logo_url=logo_url.strip() or None,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="Manufacturer",
|
||||
resource_id=created.id,
|
||||
description=audit_crud.describe_create("Manufacturer", created.id, created.name),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_manufacturers_table.html",
|
||||
@@ -564,19 +625,17 @@ async def update_manufacturer(
|
||||
url: str = Form(...),
|
||||
logo_url: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("manufacturers:write")),
|
||||
actor: User = Depends(web_require("manufacturers:write")),
|
||||
):
|
||||
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||
if m is None:
|
||||
raise HTTPException(status_code=404, detail="Manufacturer not found")
|
||||
await manufacturer_crud.update(
|
||||
db,
|
||||
m,
|
||||
{
|
||||
"name": name,
|
||||
"url": url,
|
||||
"logo_url": logo_url.strip() or None,
|
||||
},
|
||||
changes = {"name": name, "url": url, "logo_url": logo_url.strip() or None}
|
||||
await manufacturer_crud.update(db, m, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="Manufacturer",
|
||||
resource_id=manufacturer_id,
|
||||
description=audit_crud.describe_update("Manufacturer", manufacturer_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_manufacturer_row.html", {"m": m}
|
||||
@@ -587,11 +646,17 @@ async def update_manufacturer(
|
||||
async def delete_manufacturer(
|
||||
manufacturer_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("manufacturers:delete")),
|
||||
actor: User = Depends(web_require("manufacturers:delete")),
|
||||
):
|
||||
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||
if m is not None:
|
||||
name = m.name
|
||||
await manufacturer_crud.delete(db, m)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="Manufacturer",
|
||||
resource_id=manufacturer_id,
|
||||
description=audit_crud.describe_delete("Manufacturer", manufacturer_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -622,14 +687,19 @@ async def create_scope_model(
|
||||
manufacturer_id: int = Form(...),
|
||||
url: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("scope_models:write")),
|
||||
actor: User = Depends(web_require("scope_models:write")),
|
||||
):
|
||||
await scope_model_crud.create(
|
||||
created = await scope_model_crud.create(
|
||||
db,
|
||||
name=name,
|
||||
manufacturer_id=manufacturer_id,
|
||||
url=url.strip() or None,
|
||||
)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="ScopeModel",
|
||||
resource_id=created.id,
|
||||
description=audit_crud.describe_create("ScopeModel", created.id, created.name),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_scope_models_table.html",
|
||||
@@ -677,19 +747,17 @@ async def update_scope_model(
|
||||
manufacturer_id: int = Form(...),
|
||||
url: str = Form(""),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("scope_models:write")),
|
||||
actor: User = Depends(web_require("scope_models:write")),
|
||||
):
|
||||
sm = await scope_model_crud.get(db, scope_model_id)
|
||||
if sm is None:
|
||||
raise HTTPException(status_code=404, detail="Scope model not found")
|
||||
await scope_model_crud.update(
|
||||
db,
|
||||
sm,
|
||||
{
|
||||
"name": name,
|
||||
"manufacturer_id": manufacturer_id,
|
||||
"url": url.strip() or None,
|
||||
},
|
||||
changes = {"name": name, "manufacturer_id": manufacturer_id, "url": url.strip() or None}
|
||||
await scope_model_crud.update(db, sm, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="ScopeModel",
|
||||
resource_id=scope_model_id,
|
||||
description=audit_crud.describe_update("ScopeModel", scope_model_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_scope_model_row.html", {"sm": sm}
|
||||
@@ -700,11 +768,17 @@ async def update_scope_model(
|
||||
async def delete_scope_model(
|
||||
scope_model_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("scope_models:delete")),
|
||||
actor: User = Depends(web_require("scope_models:delete")),
|
||||
):
|
||||
sm = await scope_model_crud.get(db, scope_model_id)
|
||||
if sm is not None:
|
||||
name = sm.name
|
||||
await scope_model_crud.delete(db, sm)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="ScopeModel",
|
||||
resource_id=scope_model_id,
|
||||
description=audit_crud.describe_delete("ScopeModel", scope_model_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -729,10 +803,15 @@ async def create_part_type(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("part_types:write")),
|
||||
actor: User = Depends(web_require("part_types:write")),
|
||||
):
|
||||
if not await part_type_crud.get_by_name(db, name):
|
||||
await part_type_crud.create(db, name=name)
|
||||
created = await part_type_crud.create(db, name=name)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="create", resource_type="PartType",
|
||||
resource_id=created.id,
|
||||
description=audit_crud.describe_create("PartType", created.id, created.name),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/_part_types_table.html",
|
||||
@@ -776,12 +855,18 @@ async def update_part_type(
|
||||
part_type_id: int,
|
||||
name: str = Form(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("part_types:write")),
|
||||
actor: User = Depends(web_require("part_types:write")),
|
||||
):
|
||||
pt = await part_type_crud.get(db, part_type_id)
|
||||
if pt is None:
|
||||
raise HTTPException(status_code=404, detail="Part type not found")
|
||||
await part_type_crud.update(db, pt, {"name": name})
|
||||
changes = {"name": name}
|
||||
await part_type_crud.update(db, pt, changes)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="update", resource_type="PartType",
|
||||
resource_id=part_type_id,
|
||||
description=audit_crud.describe_update("PartType", part_type_id, changes),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_part_type_row.html", {"pt": pt}
|
||||
)
|
||||
@@ -791,9 +876,31 @@ async def update_part_type(
|
||||
async def delete_part_type(
|
||||
part_type_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(web_require("part_types:delete")),
|
||||
actor: User = Depends(web_require("part_types:delete")),
|
||||
):
|
||||
pt = await part_type_crud.get(db, part_type_id)
|
||||
if pt is not None:
|
||||
name = pt.name
|
||||
await part_type_crud.delete(db, pt)
|
||||
await audit_crud.log(
|
||||
db, actor=actor, action="delete", resource_type="PartType",
|
||||
resource_id=part_type_id,
|
||||
description=audit_crud.describe_delete("PartType", part_type_id, name),
|
||||
)
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# --- Audit Log ---------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/audit-log", response_class=HTMLResponse)
|
||||
async def audit_log_page(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(web_require("audit_log:read")),
|
||||
):
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"admin/audit_log.html",
|
||||
{"user": user, "entries": await audit_crud.list_recent(db)},
|
||||
)
|
||||
Reference in new issue
Block a user