Adding audit log
This commit is contained in:
1 parent
fe6f9cca05
commit
45bc5bf8f6
18 files changed
+531
-107
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
"""add_audit_logs_table
|
||||||
|
|
||||||
|
Revision ID: fd5379986da4
|
||||||
|
Revises: e23aa8dd2536
|
||||||
|
Create Date: 2026-06-25 11:06:13.028067
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'fd5379986da4'
|
||||||
|
down_revision: Union[str, None] = 'e23aa8dd2536'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
|
from app.deps import require_permission
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.audit_log import AuditLogRead
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[AuditLogRead])
|
||||||
|
async def list_audit_log(
|
||||||
|
limit: int = 200,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_: User = Depends(require_permission("audit_log:read")),
|
||||||
|
):
|
||||||
|
return await audit_crud.list_recent(db, limit=limit)
|
||||||
+25
-5
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
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 iso_thread as iso_thread_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -24,14 +25,20 @@ async def list_iso_threads(
|
|||||||
async def create_iso_thread(
|
async def create_iso_thread(
|
||||||
payload: IsoThreadCreate,
|
payload: IsoThreadCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("iso_threads:write")),
|
actor: User = Depends(require_permission("iso_threads:write")),
|
||||||
):
|
):
|
||||||
return await iso_thread_crud.create(
|
thread = await iso_thread_crud.create(
|
||||||
db,
|
db,
|
||||||
diameter_mm=payload.diameter_mm,
|
diameter_mm=payload.diameter_mm,
|
||||||
pitch_mm=payload.pitch_mm,
|
pitch_mm=payload.pitch_mm,
|
||||||
depth_mm=payload.depth_mm,
|
depth_mm=payload.depth_mm,
|
||||||
)
|
)
|
||||||
|
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 thread
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{iso_thread_id}", response_model=IsoThreadRead)
|
@router.get("/{iso_thread_id}", response_model=IsoThreadRead)
|
||||||
@@ -51,21 +58,34 @@ async def update_iso_thread(
|
|||||||
iso_thread_id: int,
|
iso_thread_id: int,
|
||||||
payload: IsoThreadUpdate,
|
payload: IsoThreadUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("iso_threads:write")),
|
actor: User = Depends(require_permission("iso_threads:write")),
|
||||||
):
|
):
|
||||||
thread = await iso_thread_crud.get(db, iso_thread_id)
|
thread = await iso_thread_crud.get(db, iso_thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ISO thread not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ISO thread not found")
|
||||||
return await iso_thread_crud.update(db, thread, payload.model_dump(exclude_unset=True))
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
thread = 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 thread
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{iso_thread_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{iso_thread_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_iso_thread(
|
async def delete_iso_thread(
|
||||||
iso_thread_id: int,
|
iso_thread_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("iso_threads:delete")),
|
actor: User = Depends(require_permission("iso_threads:delete")),
|
||||||
):
|
):
|
||||||
thread = await iso_thread_crud.get(db, iso_thread_id)
|
thread = await iso_thread_crud.get(db, iso_thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ISO thread not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ISO thread not found")
|
||||||
|
name = f"M{thread.diameter_mm}×{thread.pitch_mm}"
|
||||||
await iso_thread_crud.delete(db, thread)
|
await iso_thread_crud.delete(db, thread)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="delete", resource_type="IsoThread",
|
||||||
|
resource_id=iso_thread_id,
|
||||||
|
description=audit_crud.describe_delete("IsoThread", iso_thread_id, name),
|
||||||
|
)
|
||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import manufacturer as manufacturer_crud
|
from app.crud import manufacturer as manufacturer_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -24,14 +25,20 @@ async def list_manufacturers(
|
|||||||
async def create_manufacturer(
|
async def create_manufacturer(
|
||||||
payload: ManufacturerCreate,
|
payload: ManufacturerCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("manufacturers:write")),
|
actor: User = Depends(require_permission("manufacturers:write")),
|
||||||
):
|
):
|
||||||
return await manufacturer_crud.create(
|
m = await manufacturer_crud.create(
|
||||||
db,
|
db,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
url=str(payload.url),
|
url=str(payload.url),
|
||||||
logo_url=str(payload.logo_url) if payload.logo_url else None,
|
logo_url=str(payload.logo_url) if payload.logo_url else None,
|
||||||
)
|
)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="create", resource_type="Manufacturer",
|
||||||
|
resource_id=m.id,
|
||||||
|
description=audit_crud.describe_create("Manufacturer", m.id, m.name),
|
||||||
|
)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{manufacturer_id}", response_model=ManufacturerRead)
|
@router.get("/{manufacturer_id}", response_model=ManufacturerRead)
|
||||||
@@ -51,7 +58,7 @@ async def update_manufacturer(
|
|||||||
manufacturer_id: int,
|
manufacturer_id: int,
|
||||||
payload: ManufacturerUpdate,
|
payload: ManufacturerUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("manufacturers:write")),
|
actor: User = Depends(require_permission("manufacturers:write")),
|
||||||
):
|
):
|
||||||
m = await manufacturer_crud.get(db, manufacturer_id)
|
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||||
if m is None:
|
if m is None:
|
||||||
@@ -61,16 +68,28 @@ async def update_manufacturer(
|
|||||||
data["url"] = str(data["url"])
|
data["url"] = str(data["url"])
|
||||||
if "logo_url" in data and data["logo_url"] is not None:
|
if "logo_url" in data and data["logo_url"] is not None:
|
||||||
data["logo_url"] = str(data["logo_url"])
|
data["logo_url"] = str(data["logo_url"])
|
||||||
return await manufacturer_crud.update(db, m, data)
|
m = await manufacturer_crud.update(db, m, data)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="update", resource_type="Manufacturer",
|
||||||
|
resource_id=m.id,
|
||||||
|
description=audit_crud.describe_update("Manufacturer", m.id, data),
|
||||||
|
)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{manufacturer_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{manufacturer_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_manufacturer(
|
async def delete_manufacturer(
|
||||||
manufacturer_id: int,
|
manufacturer_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("manufacturers:delete")),
|
actor: User = Depends(require_permission("manufacturers:delete")),
|
||||||
):
|
):
|
||||||
m = await manufacturer_crud.get(db, manufacturer_id)
|
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||||
if m is None:
|
if m is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manufacturer not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Manufacturer not found")
|
||||||
|
name = m.name
|
||||||
await manufacturer_crud.delete(db, m)
|
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),
|
||||||
|
)
|
||||||
+25
-5
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import part_type as part_type_crud
|
from app.crud import part_type as part_type_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -24,9 +25,15 @@ async def list_part_types(
|
|||||||
async def create_part_type(
|
async def create_part_type(
|
||||||
payload: PartTypeCreate,
|
payload: PartTypeCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("part_types:write")),
|
actor: User = Depends(require_permission("part_types:write")),
|
||||||
):
|
):
|
||||||
return await part_type_crud.create(db, name=payload.name)
|
pt = await part_type_crud.create(db, name=payload.name)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="create", resource_type="PartType",
|
||||||
|
resource_id=pt.id,
|
||||||
|
description=audit_crud.describe_create("PartType", pt.id, pt.name),
|
||||||
|
)
|
||||||
|
return pt
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{part_type_id}", response_model=PartTypeRead)
|
@router.get("/{part_type_id}", response_model=PartTypeRead)
|
||||||
@@ -46,21 +53,34 @@ async def update_part_type(
|
|||||||
part_type_id: int,
|
part_type_id: int,
|
||||||
payload: PartTypeUpdate,
|
payload: PartTypeUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("part_types:write")),
|
actor: User = Depends(require_permission("part_types:write")),
|
||||||
):
|
):
|
||||||
pt = await part_type_crud.get(db, part_type_id)
|
pt = await part_type_crud.get(db, part_type_id)
|
||||||
if pt is None:
|
if pt is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found")
|
||||||
return await part_type_crud.update(db, pt, payload.model_dump(exclude_unset=True))
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
pt = await part_type_crud.update(db, pt, changes)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="update", resource_type="PartType",
|
||||||
|
resource_id=pt.id,
|
||||||
|
description=audit_crud.describe_update("PartType", pt.id, changes),
|
||||||
|
)
|
||||||
|
return pt
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{part_type_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{part_type_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_part_type(
|
async def delete_part_type(
|
||||||
part_type_id: int,
|
part_type_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("part_types:delete")),
|
actor: User = Depends(require_permission("part_types:delete")),
|
||||||
):
|
):
|
||||||
pt = await part_type_crud.get(db, part_type_id)
|
pt = await part_type_crud.get(db, part_type_id)
|
||||||
if pt is None:
|
if pt is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Part type not found")
|
||||||
|
name = pt.name
|
||||||
await part_type_crud.delete(db, pt)
|
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),
|
||||||
|
)
|
||||||
+25
-5
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import role as role_crud
|
from app.crud import role as role_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -22,18 +23,24 @@ async def list_roles(
|
|||||||
async def create_role(
|
async def create_role(
|
||||||
payload: RoleCreate,
|
payload: RoleCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("roles:write")),
|
actor: User = Depends(require_permission("roles:write")),
|
||||||
):
|
):
|
||||||
if await role_crud.get_by_name(db, payload.name):
|
if await role_crud.get_by_name(db, payload.name):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Role already exists"
|
status_code=status.HTTP_409_CONFLICT, detail="Role already exists"
|
||||||
)
|
)
|
||||||
return await role_crud.create(
|
role = await role_crud.create(
|
||||||
db,
|
db,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
description=payload.description,
|
description=payload.description,
|
||||||
permission_ids=payload.permission_ids,
|
permission_ids=payload.permission_ids,
|
||||||
)
|
)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="create", resource_type="Role",
|
||||||
|
resource_id=role.id,
|
||||||
|
description=audit_crud.describe_create("Role", role.id, role.name),
|
||||||
|
)
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
@router.get("/permissions", response_model=list[PermissionRead])
|
@router.get("/permissions", response_model=list[PermissionRead])
|
||||||
@@ -63,25 +70,38 @@ async def update_role(
|
|||||||
role_id: int,
|
role_id: int,
|
||||||
payload: RoleUpdate,
|
payload: RoleUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("roles:write")),
|
actor: User = Depends(require_permission("roles:write")),
|
||||||
):
|
):
|
||||||
role = await role_crud.get(db, role_id)
|
role = await role_crud.get(db, role_id)
|
||||||
if role is None:
|
if role is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
|
||||||
)
|
)
|
||||||
return await role_crud.update(db, role, payload.model_dump(exclude_unset=True))
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
role = 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 role
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_role(
|
async def delete_role(
|
||||||
role_id: int,
|
role_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("roles:write")),
|
actor: User = Depends(require_permission("roles:write")),
|
||||||
):
|
):
|
||||||
role = await role_crud.get(db, role_id)
|
role = await role_crud.get(db, role_id)
|
||||||
if role is None:
|
if role is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="Role not found"
|
||||||
)
|
)
|
||||||
|
name = role.name
|
||||||
await role_crud.delete(db, role)
|
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),
|
||||||
|
)
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api import auth, iso_threads, manufacturers, part_types, roles, scope_models, unc_threads, users
|
from app.api import audit_log, auth, iso_threads, manufacturers, part_types, roles, scope_models, unc_threads, users
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
@@ -11,3 +11,4 @@ api_router.include_router(unc_threads.router, prefix="/unc-threads", tags=["unc-
|
|||||||
api_router.include_router(manufacturers.router, prefix="/manufacturers", tags=["manufacturers"])
|
api_router.include_router(manufacturers.router, prefix="/manufacturers", tags=["manufacturers"])
|
||||||
api_router.include_router(scope_models.router, prefix="/scope-models", tags=["scope-models"])
|
api_router.include_router(scope_models.router, prefix="/scope-models", tags=["scope-models"])
|
||||||
api_router.include_router(part_types.router, prefix="/part-types", tags=["part-types"])
|
api_router.include_router(part_types.router, prefix="/part-types", tags=["part-types"])
|
||||||
|
api_router.include_router(audit_log.router, prefix="/audit-log", tags=["audit-log"])
|
||||||
+24
-5
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import scope_model as scope_model_crud
|
from app.crud import scope_model as scope_model_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -24,14 +25,20 @@ async def list_scope_models(
|
|||||||
async def create_scope_model(
|
async def create_scope_model(
|
||||||
payload: ScopeModelCreate,
|
payload: ScopeModelCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("scope_models:write")),
|
actor: User = Depends(require_permission("scope_models:write")),
|
||||||
):
|
):
|
||||||
return await scope_model_crud.create(
|
sm = await scope_model_crud.create(
|
||||||
db,
|
db,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
manufacturer_id=payload.manufacturer_id,
|
manufacturer_id=payload.manufacturer_id,
|
||||||
url=str(payload.url) if payload.url else None,
|
url=str(payload.url) if payload.url else None,
|
||||||
)
|
)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="create", resource_type="ScopeModel",
|
||||||
|
resource_id=sm.id,
|
||||||
|
description=audit_crud.describe_create("ScopeModel", sm.id, sm.name),
|
||||||
|
)
|
||||||
|
return sm
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{scope_model_id}", response_model=ScopeModelRead)
|
@router.get("/{scope_model_id}", response_model=ScopeModelRead)
|
||||||
@@ -51,7 +58,7 @@ async def update_scope_model(
|
|||||||
scope_model_id: int,
|
scope_model_id: int,
|
||||||
payload: ScopeModelUpdate,
|
payload: ScopeModelUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("scope_models:write")),
|
actor: User = Depends(require_permission("scope_models:write")),
|
||||||
):
|
):
|
||||||
sm = await scope_model_crud.get(db, scope_model_id)
|
sm = await scope_model_crud.get(db, scope_model_id)
|
||||||
if sm is None:
|
if sm is None:
|
||||||
@@ -59,16 +66,28 @@ async def update_scope_model(
|
|||||||
data = payload.model_dump(exclude_unset=True)
|
data = payload.model_dump(exclude_unset=True)
|
||||||
if "url" in data and data["url"] is not None:
|
if "url" in data and data["url"] is not None:
|
||||||
data["url"] = str(data["url"])
|
data["url"] = str(data["url"])
|
||||||
return await scope_model_crud.update(db, sm, data)
|
sm = await scope_model_crud.update(db, sm, data)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="update", resource_type="ScopeModel",
|
||||||
|
resource_id=sm.id,
|
||||||
|
description=audit_crud.describe_update("ScopeModel", sm.id, data),
|
||||||
|
)
|
||||||
|
return sm
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{scope_model_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{scope_model_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_scope_model(
|
async def delete_scope_model(
|
||||||
scope_model_id: int,
|
scope_model_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("scope_models:delete")),
|
actor: User = Depends(require_permission("scope_models:delete")),
|
||||||
):
|
):
|
||||||
sm = await scope_model_crud.get(db, scope_model_id)
|
sm = await scope_model_crud.get(db, scope_model_id)
|
||||||
if sm is None:
|
if sm is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scope model not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Scope model not found")
|
||||||
|
name = sm.name
|
||||||
await scope_model_crud.delete(db, sm)
|
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),
|
||||||
|
)
|
||||||
+25
-5
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import unc_thread as unc_thread_crud
|
from app.crud import unc_thread as unc_thread_crud
|
||||||
from app.deps import require_permission
|
from app.deps import require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -24,15 +25,21 @@ async def list_unc_threads(
|
|||||||
async def create_unc_thread(
|
async def create_unc_thread(
|
||||||
payload: UncThreadCreate,
|
payload: UncThreadCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("unc_threads:write")),
|
actor: User = Depends(require_permission("unc_threads:write")),
|
||||||
):
|
):
|
||||||
return await unc_thread_crud.create(
|
thread = await unc_thread_crud.create(
|
||||||
db,
|
db,
|
||||||
diameter_fraction=payload.diameter_fraction,
|
diameter_fraction=payload.diameter_fraction,
|
||||||
tpi=payload.tpi,
|
tpi=payload.tpi,
|
||||||
series=payload.series,
|
series=payload.series,
|
||||||
depth_in=payload.depth_in,
|
depth_in=payload.depth_in,
|
||||||
)
|
)
|
||||||
|
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 thread
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{unc_thread_id}", response_model=UncThreadRead)
|
@router.get("/{unc_thread_id}", response_model=UncThreadRead)
|
||||||
@@ -52,21 +59,34 @@ async def update_unc_thread(
|
|||||||
unc_thread_id: int,
|
unc_thread_id: int,
|
||||||
payload: UncThreadUpdate,
|
payload: UncThreadUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("unc_threads:write")),
|
actor: User = Depends(require_permission("unc_threads:write")),
|
||||||
):
|
):
|
||||||
thread = await unc_thread_crud.get(db, unc_thread_id)
|
thread = await unc_thread_crud.get(db, unc_thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UNC thread not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UNC thread not found")
|
||||||
return await unc_thread_crud.update(db, thread, payload.model_dump(exclude_unset=True))
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
thread = 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 thread
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{unc_thread_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{unc_thread_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_unc_thread(
|
async def delete_unc_thread(
|
||||||
unc_thread_id: int,
|
unc_thread_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("unc_threads:delete")),
|
actor: User = Depends(require_permission("unc_threads:delete")),
|
||||||
):
|
):
|
||||||
thread = await unc_thread_crud.get(db, unc_thread_id)
|
thread = await unc_thread_crud.get(db, unc_thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UNC thread not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UNC thread not found")
|
||||||
|
name = f"{thread.diameter_fraction}-{thread.tpi} {thread.series.value}"
|
||||||
await unc_thread_crud.delete(db, thread)
|
await unc_thread_crud.delete(db, thread)
|
||||||
|
await audit_crud.log(
|
||||||
|
db, actor=actor, action="delete", resource_type="UncThread",
|
||||||
|
resource_id=unc_thread_id,
|
||||||
|
description=audit_crud.describe_delete("UncThread", unc_thread_id, name),
|
||||||
|
)
|
||||||
+33
-13
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
|
from app.crud import audit as audit_crud
|
||||||
from app.crud import user as user_crud
|
from app.crud import user as user_crud
|
||||||
from app.deps import get_current_user, require_permission
|
from app.deps import get_current_user, require_permission
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -29,13 +30,13 @@ async def list_users(
|
|||||||
async def create_user(
|
async def create_user(
|
||||||
payload: UserCreate,
|
payload: UserCreate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("users:write")),
|
actor: User = Depends(require_permission("users:write")),
|
||||||
):
|
):
|
||||||
if await user_crud.get_by_email(db, payload.email):
|
if await user_crud.get_by_email(db, payload.email):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Email already registered"
|
status_code=status.HTTP_409_CONFLICT, detail="Email already registered"
|
||||||
)
|
)
|
||||||
return await user_crud.create(
|
created = await user_crud.create(
|
||||||
db,
|
db,
|
||||||
email=payload.email,
|
email=payload.email,
|
||||||
password=payload.password,
|
password=payload.password,
|
||||||
@@ -43,6 +44,12 @@ async def create_user(
|
|||||||
is_superuser=payload.is_superuser,
|
is_superuser=payload.is_superuser,
|
||||||
role_ids=payload.role_ids,
|
role_ids=payload.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 created
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{user_id}", response_model=UserRead)
|
@router.get("/{user_id}", response_model=UserRead)
|
||||||
@@ -51,12 +58,12 @@ async def get_user(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("users:read")),
|
_: User = Depends(require_permission("users:read")),
|
||||||
):
|
):
|
||||||
user = await user_crud.get(db, user_id)
|
target = await user_crud.get(db, user_id)
|
||||||
if user is None:
|
if target is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||||
)
|
)
|
||||||
return user
|
return target
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{user_id}", response_model=UserRead)
|
@router.patch("/{user_id}", response_model=UserRead)
|
||||||
@@ -64,25 +71,38 @@ async def update_user(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
payload: UserUpdate,
|
payload: UserUpdate,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("users:write")),
|
actor: User = Depends(require_permission("users:write")),
|
||||||
):
|
):
|
||||||
user = await user_crud.get(db, user_id)
|
target = await user_crud.get(db, user_id)
|
||||||
if user is None:
|
if target is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||||
)
|
)
|
||||||
return await user_crud.update(db, user, payload.model_dump(exclude_unset=True))
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
target = 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 target
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_user(
|
async def delete_user(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_: User = Depends(require_permission("users:delete")),
|
actor: User = Depends(require_permission("users:delete")),
|
||||||
):
|
):
|
||||||
user = await user_crud.get(db, user_id)
|
target = await user_crud.get(db, user_id)
|
||||||
if user is None:
|
if target is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||||
)
|
)
|
||||||
await user_crud.delete(db, user)
|
email = target.email
|
||||||
|
await user_crud.delete(db, target)
|
||||||
|
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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.audit_log import AuditLog
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
def describe_create(resource_type: str, resource_id: int, name: str) -> str:
|
||||||
|
return f"Created {resource_type} '{name}' (#{resource_id})"
|
||||||
|
|
||||||
|
|
||||||
|
def describe_update(resource_type: str, resource_id: int, changes: dict) -> str:
|
||||||
|
safe = {k: ("***" if k == "password" else v) for k, v in changes.items()}
|
||||||
|
parts = ", ".join(f"{k}={v!r}" for k, v in safe.items())
|
||||||
|
suffix = f": {parts}" if parts else ""
|
||||||
|
return f"Updated {resource_type} #{resource_id}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def describe_delete(resource_type: str, resource_id: int, name: str) -> str:
|
||||||
|
return f"Deleted {resource_type} '{name}' (#{resource_id})"
|
||||||
|
|
||||||
|
|
||||||
|
async def log(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
actor: User,
|
||||||
|
action: str,
|
||||||
|
resource_type: str,
|
||||||
|
resource_id: int | None,
|
||||||
|
description: str,
|
||||||
|
) -> None:
|
||||||
|
entry = AuditLog(
|
||||||
|
user_id=actor.id,
|
||||||
|
user_email=actor.email,
|
||||||
|
action=action,
|
||||||
|
resource_type=resource_type,
|
||||||
|
resource_id=resource_id,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
db.add(entry)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def list_recent(db: AsyncSession, limit: int = 200) -> list[AuditLog]:
|
||||||
|
result = await db.execute(
|
||||||
|
select(AuditLog).order_by(AuditLog.created_at.desc()).limit(limit)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Import all models so they register with ``Base.metadata``."""
|
"""Import all models so they register with ``Base.metadata``."""
|
||||||
|
|
||||||
from app.models.associations import role_permissions, user_roles
|
from app.models.associations import role_permissions, user_roles
|
||||||
|
from app.models.audit_log import AuditLog
|
||||||
from app.models.iso_thread import IsoThread
|
from app.models.iso_thread import IsoThread
|
||||||
from app.models.manufacturer import Manufacturer
|
from app.models.manufacturer import Manufacturer
|
||||||
from app.models.part_type import PartType
|
from app.models.part_type import PartType
|
||||||
@@ -12,6 +13,7 @@ from app.models.unc_thread import UncThread
|
|||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AuditLog",
|
||||||
"IsoThread",
|
"IsoThread",
|
||||||
"Manufacturer",
|
"Manufacturer",
|
||||||
"PartType",
|
"PartType",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLog(Base):
|
||||||
|
__tablename__ = "audit_logs"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), index=True
|
||||||
|
)
|
||||||
|
user_id: Mapped[int | None] = mapped_column(
|
||||||
|
ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True
|
||||||
|
)
|
||||||
|
user_email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
action: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
resource_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
resource_id: Mapped[int | None] = mapped_column(nullable=True)
|
||||||
|
description: Mapped[str] = mapped_column(nullable=False)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogRead(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
user_id: int | None
|
||||||
|
user_email: str
|
||||||
|
action: str
|
||||||
|
resource_type: str
|
||||||
|
resource_id: int | None
|
||||||
|
description: str
|
||||||
@@ -23,6 +23,9 @@
|
|||||||
{% if user.is_superuser or "part_types:read" in user.permissions %}
|
{% if user.is_superuser or "part_types:read" in user.permissions %}
|
||||||
<a href="/admin/part-types">Part Types</a>
|
<a href="/admin/part-types">Part Types</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if user.is_superuser or "audit_log:read" in user.permissions %}
|
||||||
|
<a href="/admin/audit-log">Audit Log</a>
|
||||||
|
{% endif %}
|
||||||
<a href="/docs" target="_blank">API docs</a>
|
<a href="/docs" target="_blank">API docs</a>
|
||||||
</nav>
|
</nav>
|
||||||
<form method="post" action="/logout" class="logout">
|
<form method="post" action="/logout" class="logout">
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Audit Log · {{ app_name }}{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% include "_nav.html" %}
|
||||||
|
<main class="container">
|
||||||
|
<h1>Audit Log</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
{% if entries %}
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Resource</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<tr>
|
||||||
|
<td class="muted" style="white-space:nowrap">{{ entry.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||||
|
<td>{{ entry.user_email }}</td>
|
||||||
|
<td><span class="badge badge-{{ entry.action }}">{{ entry.action }}</span></td>
|
||||||
|
<td>{{ entry.resource_type }}{% if entry.resource_id %} #{{ entry.resource_id }}{% endif %}</td>
|
||||||
|
<td>{{ entry.description }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted">No audit log entries yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
+169
-62
@@ -5,6 +5,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
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 iso_thread as iso_thread_crud
|
||||||
from app.crud import manufacturer as manufacturer_crud
|
from app.crud import manufacturer as manufacturer_crud
|
||||||
from app.crud import part_type as part_type_crud
|
from app.crud import part_type as part_type_crud
|
||||||
@@ -68,10 +69,10 @@ async def create_user(
|
|||||||
is_superuser: bool = Form(False),
|
is_superuser: bool = Form(False),
|
||||||
role_ids: list[int] = Form(default=[]),
|
role_ids: list[int] = Form(default=[]),
|
||||||
db: AsyncSession = Depends(get_db),
|
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):
|
if not await user_crud.get_by_email(db, email):
|
||||||
await user_crud.create(
|
created = await user_crud.create(
|
||||||
db,
|
db,
|
||||||
email=email,
|
email=email,
|
||||||
password=password,
|
password=password,
|
||||||
@@ -79,6 +80,11 @@ async def create_user(
|
|||||||
is_superuser=is_superuser,
|
is_superuser=is_superuser,
|
||||||
role_ids=role_ids,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_users_table.html",
|
"admin/_users_table.html",
|
||||||
@@ -129,21 +135,23 @@ async def update_user(
|
|||||||
is_superuser: bool = Form(False),
|
is_superuser: bool = Form(False),
|
||||||
role_ids: list[int] = Form(default=[]),
|
role_ids: list[int] = Form(default=[]),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
target = await user_crud.get(db, user_id)
|
||||||
if target is None:
|
if target is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
await user_crud.update(
|
changes = {
|
||||||
db,
|
|
||||||
target,
|
|
||||||
{
|
|
||||||
"full_name": full_name or None,
|
"full_name": full_name or None,
|
||||||
"password": password or None,
|
"password": password or None,
|
||||||
"is_active": is_active,
|
"is_active": is_active,
|
||||||
"is_superuser": is_superuser,
|
"is_superuser": is_superuser,
|
||||||
"role_ids": role_ids,
|
"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})
|
return templates.TemplateResponse(request, "admin/_user_row.html", {"u": target})
|
||||||
|
|
||||||
@@ -152,12 +160,17 @@ async def update_user(
|
|||||||
async def delete_user(
|
async def delete_user(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
target = await user_crud.get(db, user_id)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
|
email = target.email
|
||||||
await user_crud.delete(db, target)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -188,15 +201,20 @@ async def create_role(
|
|||||||
description: str = Form(""),
|
description: str = Form(""),
|
||||||
permission_ids: list[int] = Form(default=[]),
|
permission_ids: list[int] = Form(default=[]),
|
||||||
db: AsyncSession = Depends(get_db),
|
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):
|
if not await role_crud.get_by_name(db, name):
|
||||||
await role_crud.create(
|
created = await role_crud.create(
|
||||||
db,
|
db,
|
||||||
name=name,
|
name=name,
|
||||||
description=description or None,
|
description=description or None,
|
||||||
permission_ids=permission_ids,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_roles_table.html",
|
"admin/_roles_table.html",
|
||||||
@@ -243,12 +261,18 @@ async def update_role(
|
|||||||
role_id: int,
|
role_id: int,
|
||||||
permission_ids: list[int] = Form(default=[]),
|
permission_ids: list[int] = Form(default=[]),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
role = await role_crud.get(db, role_id)
|
||||||
if role is None:
|
if role is None:
|
||||||
raise HTTPException(status_code=404, detail="Role not found")
|
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})
|
return templates.TemplateResponse(request, "admin/_role_row.html", {"r": role})
|
||||||
|
|
||||||
|
|
||||||
@@ -256,11 +280,17 @@ async def update_role(
|
|||||||
async def delete_role(
|
async def delete_role(
|
||||||
role_id: int,
|
role_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
role = await role_crud.get(db, role_id)
|
||||||
if role is not None:
|
if role is not None:
|
||||||
|
name = role.name
|
||||||
await role_crud.delete(db, role)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -287,14 +317,19 @@ async def create_iso_thread(
|
|||||||
pitch_mm: float = Form(...),
|
pitch_mm: float = Form(...),
|
||||||
depth_mm: str = Form(""),
|
depth_mm: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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,
|
db,
|
||||||
diameter_mm=diameter_mm,
|
diameter_mm=diameter_mm,
|
||||||
pitch_mm=pitch_mm,
|
pitch_mm=pitch_mm,
|
||||||
depth_mm=float(depth_mm) if depth_mm.strip() else None,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_iso_threads_table.html",
|
"admin/_iso_threads_table.html",
|
||||||
@@ -340,19 +375,21 @@ async def update_iso_thread(
|
|||||||
pitch_mm: float = Form(...),
|
pitch_mm: float = Form(...),
|
||||||
depth_mm: str = Form(""),
|
depth_mm: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
thread = await iso_thread_crud.get(db, thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=404, detail="ISO thread not found")
|
raise HTTPException(status_code=404, detail="ISO thread not found")
|
||||||
await iso_thread_crud.update(
|
changes = {
|
||||||
db,
|
|
||||||
thread,
|
|
||||||
{
|
|
||||||
"diameter_mm": diameter_mm,
|
"diameter_mm": diameter_mm,
|
||||||
"pitch_mm": pitch_mm,
|
"pitch_mm": pitch_mm,
|
||||||
"depth_mm": float(depth_mm) if depth_mm.strip() else None,
|
"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(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_iso_thread_row.html", {"t": thread}
|
request, "admin/_iso_thread_row.html", {"t": thread}
|
||||||
@@ -363,11 +400,17 @@ async def update_iso_thread(
|
|||||||
async def delete_iso_thread(
|
async def delete_iso_thread(
|
||||||
thread_id: int,
|
thread_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
thread = await iso_thread_crud.get(db, thread_id)
|
||||||
if thread is not None:
|
if thread is not None:
|
||||||
|
name = f"M{thread.diameter_mm}×{thread.pitch_mm}"
|
||||||
await iso_thread_crud.delete(db, thread)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -399,15 +442,20 @@ async def create_unc_thread(
|
|||||||
series: UncSeries = Form(...),
|
series: UncSeries = Form(...),
|
||||||
depth_in: str = Form(""),
|
depth_in: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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,
|
db,
|
||||||
diameter_fraction=diameter_fraction,
|
diameter_fraction=diameter_fraction,
|
||||||
tpi=tpi,
|
tpi=tpi,
|
||||||
series=series,
|
series=series,
|
||||||
depth_in=float(depth_in) if depth_in.strip() else None,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_unc_threads_table.html",
|
"admin/_unc_threads_table.html",
|
||||||
@@ -456,20 +504,22 @@ async def update_unc_thread(
|
|||||||
series: UncSeries = Form(...),
|
series: UncSeries = Form(...),
|
||||||
depth_in: str = Form(""),
|
depth_in: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
thread = await unc_thread_crud.get(db, thread_id)
|
||||||
if thread is None:
|
if thread is None:
|
||||||
raise HTTPException(status_code=404, detail="UNC thread not found")
|
raise HTTPException(status_code=404, detail="UNC thread not found")
|
||||||
await unc_thread_crud.update(
|
changes = {
|
||||||
db,
|
|
||||||
thread,
|
|
||||||
{
|
|
||||||
"diameter_fraction": diameter_fraction,
|
"diameter_fraction": diameter_fraction,
|
||||||
"tpi": tpi,
|
"tpi": tpi,
|
||||||
"series": series,
|
"series": series,
|
||||||
"depth_in": float(depth_in) if depth_in.strip() else None,
|
"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(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_unc_thread_row.html", {"t": thread}
|
request, "admin/_unc_thread_row.html", {"t": thread}
|
||||||
@@ -480,11 +530,17 @@ async def update_unc_thread(
|
|||||||
async def delete_unc_thread(
|
async def delete_unc_thread(
|
||||||
thread_id: int,
|
thread_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
thread = await unc_thread_crud.get(db, thread_id)
|
||||||
if thread is not None:
|
if thread is not None:
|
||||||
|
name = f"{thread.diameter_fraction}-{thread.tpi} {thread.series.value}"
|
||||||
await unc_thread_crud.delete(db, thread)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -511,14 +567,19 @@ async def create_manufacturer(
|
|||||||
url: str = Form(...),
|
url: str = Form(...),
|
||||||
logo_url: str = Form(""),
|
logo_url: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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,
|
db,
|
||||||
name=name,
|
name=name,
|
||||||
url=url,
|
url=url,
|
||||||
logo_url=logo_url.strip() or None,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_manufacturers_table.html",
|
"admin/_manufacturers_table.html",
|
||||||
@@ -564,19 +625,17 @@ async def update_manufacturer(
|
|||||||
url: str = Form(...),
|
url: str = Form(...),
|
||||||
logo_url: str = Form(""),
|
logo_url: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||||
if m is None:
|
if m is None:
|
||||||
raise HTTPException(status_code=404, detail="Manufacturer not found")
|
raise HTTPException(status_code=404, detail="Manufacturer not found")
|
||||||
await manufacturer_crud.update(
|
changes = {"name": name, "url": url, "logo_url": logo_url.strip() or None}
|
||||||
db,
|
await manufacturer_crud.update(db, m, changes)
|
||||||
m,
|
await audit_crud.log(
|
||||||
{
|
db, actor=actor, action="update", resource_type="Manufacturer",
|
||||||
"name": name,
|
resource_id=manufacturer_id,
|
||||||
"url": url,
|
description=audit_crud.describe_update("Manufacturer", manufacturer_id, changes),
|
||||||
"logo_url": logo_url.strip() or None,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_manufacturer_row.html", {"m": m}
|
request, "admin/_manufacturer_row.html", {"m": m}
|
||||||
@@ -587,11 +646,17 @@ async def update_manufacturer(
|
|||||||
async def delete_manufacturer(
|
async def delete_manufacturer(
|
||||||
manufacturer_id: int,
|
manufacturer_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
m = await manufacturer_crud.get(db, manufacturer_id)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
|
name = m.name
|
||||||
await manufacturer_crud.delete(db, m)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -622,14 +687,19 @@ async def create_scope_model(
|
|||||||
manufacturer_id: int = Form(...),
|
manufacturer_id: int = Form(...),
|
||||||
url: str = Form(""),
|
url: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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,
|
db,
|
||||||
name=name,
|
name=name,
|
||||||
manufacturer_id=manufacturer_id,
|
manufacturer_id=manufacturer_id,
|
||||||
url=url.strip() or None,
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_scope_models_table.html",
|
"admin/_scope_models_table.html",
|
||||||
@@ -677,19 +747,17 @@ async def update_scope_model(
|
|||||||
manufacturer_id: int = Form(...),
|
manufacturer_id: int = Form(...),
|
||||||
url: str = Form(""),
|
url: str = Form(""),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
sm = await scope_model_crud.get(db, scope_model_id)
|
||||||
if sm is None:
|
if sm is None:
|
||||||
raise HTTPException(status_code=404, detail="Scope model not found")
|
raise HTTPException(status_code=404, detail="Scope model not found")
|
||||||
await scope_model_crud.update(
|
changes = {"name": name, "manufacturer_id": manufacturer_id, "url": url.strip() or None}
|
||||||
db,
|
await scope_model_crud.update(db, sm, changes)
|
||||||
sm,
|
await audit_crud.log(
|
||||||
{
|
db, actor=actor, action="update", resource_type="ScopeModel",
|
||||||
"name": name,
|
resource_id=scope_model_id,
|
||||||
"manufacturer_id": manufacturer_id,
|
description=audit_crud.describe_update("ScopeModel", scope_model_id, changes),
|
||||||
"url": url.strip() or None,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_scope_model_row.html", {"sm": sm}
|
request, "admin/_scope_model_row.html", {"sm": sm}
|
||||||
@@ -700,11 +768,17 @@ async def update_scope_model(
|
|||||||
async def delete_scope_model(
|
async def delete_scope_model(
|
||||||
scope_model_id: int,
|
scope_model_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
sm = await scope_model_crud.get(db, scope_model_id)
|
||||||
if sm is not None:
|
if sm is not None:
|
||||||
|
name = sm.name
|
||||||
await scope_model_crud.delete(db, sm)
|
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)
|
return Response(status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@@ -729,10 +803,15 @@ async def create_part_type(
|
|||||||
request: Request,
|
request: Request,
|
||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
db: AsyncSession = Depends(get_db),
|
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):
|
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(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"admin/_part_types_table.html",
|
"admin/_part_types_table.html",
|
||||||
@@ -776,12 +855,18 @@ async def update_part_type(
|
|||||||
part_type_id: int,
|
part_type_id: int,
|
||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
pt = await part_type_crud.get(db, part_type_id)
|
||||||
if pt is None:
|
if pt is None:
|
||||||
raise HTTPException(status_code=404, detail="Part type not found")
|
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(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_part_type_row.html", {"pt": pt}
|
request, "admin/_part_type_row.html", {"pt": pt}
|
||||||
)
|
)
|
||||||
@@ -791,9 +876,31 @@ async def update_part_type(
|
|||||||
async def delete_part_type(
|
async def delete_part_type(
|
||||||
part_type_id: int,
|
part_type_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
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)
|
pt = await part_type_crud.get(db, part_type_id)
|
||||||
if pt is not None:
|
if pt is not None:
|
||||||
|
name = pt.name
|
||||||
await part_type_crud.delete(db, pt)
|
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)
|
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)},
|
||||||
|
)
|
||||||
+2
-1
@@ -39,11 +39,12 @@ PERMISSIONS = [
|
|||||||
("part_types:read", "View part types"),
|
("part_types:read", "View part types"),
|
||||||
("part_types:write", "Create and update part types"),
|
("part_types:write", "Create and update part types"),
|
||||||
("part_types:delete", "Delete part types"),
|
("part_types:delete", "Delete part types"),
|
||||||
|
("audit_log:read", "View audit log"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Default roles -> permission names. "admin" gets everything.
|
# Default roles -> permission names. "admin" gets everything.
|
||||||
ROLES = {
|
ROLES = {
|
||||||
"admin": [name for name, _ in PERMISSIONS],
|
"admin": [name for name, _ in PERMISSIONS], # includes audit_log:read
|
||||||
"user": ["users:read", "iso_threads:read", "unc_threads:read", "manufacturers:read", "scope_models:read", "part_types:read"],
|
"user": ["users:read", "iso_threads:read", "unc_threads:read", "manufacturers:read", "scope_models:read", "part_types:read"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user