fix: audit logs for tenants, user, roles, modules and environment and modules of tenant and modules.
2 migration scripts
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
"""create audit logs table
|
||||||
|
|
||||||
|
Revision ID: 73b754d5b2c5
|
||||||
|
Revises: c37ba6143f83
|
||||||
|
Create Date: 2026-03-30 15:43:16.849980
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '73b754d5b2c5'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'c37ba6143f83'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('audit_logs',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('module_name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('action_type', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_audit_logs_id'), 'audit_logs', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_audit_logs_module_name'), 'audit_logs', ['module_name'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_audit_logs_module_name'), table_name='audit_logs')
|
||||||
|
op.drop_index(op.f('ix_audit_logs_id'), table_name='audit_logs')
|
||||||
|
op.drop_table('audit_logs')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""add_audit_log_extra_columns
|
||||||
|
|
||||||
|
Revision ID: f9cf173f48f9
|
||||||
|
Revises: 73b754d5b2c5
|
||||||
|
Create Date: 2026-03-31 13:57:11.328101
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'f9cf173f48f9'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '73b754d5b2c5'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('audit_logs', sa.Column('entity_id', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('entity_name', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('performed_by_id', sa.UUID(), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('performed_by_email', sa.String(length=255), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('ip_address', sa.String(length=45), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('old_values', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||||
|
op.add_column('audit_logs', sa.Column('new_values', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||||
|
op.create_index(op.f('ix_audit_logs_action_type'), 'audit_logs', ['action_type'], unique=False)
|
||||||
|
op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False)
|
||||||
|
op.create_index(op.f('ix_audit_logs_performed_by_email'), 'audit_logs', ['performed_by_email'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_audit_logs_performed_by_email'), table_name='audit_logs')
|
||||||
|
op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs')
|
||||||
|
op.drop_index(op.f('ix_audit_logs_action_type'), table_name='audit_logs')
|
||||||
|
op.drop_column('audit_logs', 'new_values')
|
||||||
|
op.drop_column('audit_logs', 'old_values')
|
||||||
|
op.drop_column('audit_logs', 'ip_address')
|
||||||
|
op.drop_column('audit_logs', 'performed_by_email')
|
||||||
|
op.drop_column('audit_logs', 'performed_by_id')
|
||||||
|
op.drop_column('audit_logs', 'entity_name')
|
||||||
|
op.drop_column('audit_logs', 'entity_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
+4
-1
@@ -6,6 +6,7 @@ import logging
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from app.config.settings import settings
|
from app.config.settings import settings
|
||||||
from app.config.database import engine
|
from app.config.database import engine
|
||||||
|
from app.routes.admin import audit_logs
|
||||||
import app.models.auth.user_model
|
import app.models.auth.user_model
|
||||||
import app.models.auth.role_model
|
import app.models.auth.role_model
|
||||||
import app.models.auth.tenant_model
|
import app.models.auth.tenant_model
|
||||||
@@ -16,6 +17,7 @@ import app.models.auth.tenant_module_model
|
|||||||
import app.models.auth.sso_grant_model
|
import app.models.auth.sso_grant_model
|
||||||
import app.models.auth.access_model
|
import app.models.auth.access_model
|
||||||
import app.models.system.event_log_model
|
import app.models.system.event_log_model
|
||||||
|
import app.models.system.audit_log
|
||||||
import asyncio
|
import asyncio
|
||||||
from app.services.auth.event_service import EventService
|
from app.services.auth.event_service import EventService
|
||||||
from app.config.database import SessionLocal
|
from app.config.database import SessionLocal
|
||||||
@@ -206,7 +208,8 @@ def create_app() -> FastAPI:
|
|||||||
from app.routes.admin.modules import router as admin_modules_router
|
from app.routes.admin.modules import router as admin_modules_router
|
||||||
from app.routes.admin.module_environments import router as admin_module_env_router
|
from app.routes.admin.module_environments import router as admin_module_env_router
|
||||||
from app.routes.admin.tenant_modules import router as admin_tenant_modules_router
|
from app.routes.admin.tenant_modules import router as admin_tenant_modules_router
|
||||||
|
|
||||||
|
app.include_router(audit_logs.router, prefix="/api/admin/audit-logs", tags=["Admin - Audit Logs"])
|
||||||
app.include_router(admin_modules_router, prefix="/api/admin/modules", tags=["Admin - Modules"])
|
app.include_router(admin_modules_router, prefix="/api/admin/modules", tags=["Admin - Modules"])
|
||||||
app.include_router(admin_module_env_router, prefix="/api/admin/modules", tags=["Admin - Module Environments"])
|
app.include_router(admin_module_env_router, prefix="/api/admin/modules", tags=["Admin - Module Environments"])
|
||||||
app.include_router(admin_tenant_modules_router, prefix="/api/admin/tenants", tags=["Admin - Tenant Modules"])
|
app.include_router(admin_tenant_modules_router, prefix="/api/admin/tenants", tags=["Admin - Tenant Modules"])
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from .helpers import get_client_ip
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
def get_client_ip(request: Request) -> str:
|
||||||
|
forwarded = request.headers.get("X-Forwarded-For")
|
||||||
|
if forwarded:
|
||||||
|
return forwarded.split(",")[0].strip()
|
||||||
|
return request.client.host if request.client else "unknown"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlalchemy import Column, String, DateTime, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.config.database import Base
|
||||||
|
|
||||||
|
class AuditLog(Base):
|
||||||
|
__tablename__ = "audit_logs"
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||||
|
|
||||||
|
module_name = Column(String(100), nullable=False, index=True)
|
||||||
|
|
||||||
|
action_type = Column(String(20), nullable=False, index=True)
|
||||||
|
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
entity_id = Column(String(255), nullable=True)
|
||||||
|
|
||||||
|
entity_name = Column(String(255), nullable=True)
|
||||||
|
|
||||||
|
performed_by_id = Column(UUID(as_uuid=True), nullable=True)
|
||||||
|
performed_by_email = Column(String(255), nullable=True, index=True)
|
||||||
|
|
||||||
|
ip_address = Column(String(45), nullable=True)
|
||||||
|
|
||||||
|
old_values = Column(JSONB, nullable=True)
|
||||||
|
|
||||||
|
new_values = Column(JSONB, nullable=True)
|
||||||
|
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<AuditLog {self.action_type} on {self.module_name}"
|
||||||
|
f" by {self.performed_by_email}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.config.database import get_db
|
||||||
|
from app.middleware.auth_middleware import get_current_user, User
|
||||||
|
from app.models.system.audit_log import AuditLog
|
||||||
|
from app.schemas.auth.audit_schema import AuditLogListResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=AuditLogListResponse)
|
||||||
|
def get_audit_logs(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
module_name: Optional[str] = Query(None, description="Filter by module name"),
|
||||||
|
action_type: Optional[str] = Query(None, description="CREATE | UPDATE | DELETE"),
|
||||||
|
performed_by_email: Optional[str] = Query(None, description="Filter by actor email"),
|
||||||
|
limit: int = Query(50, ge=1, le=500),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Fetch audit logs with optional filters.
|
||||||
|
Only accessible to authenticated users (super-admin in practice).
|
||||||
|
"""
|
||||||
|
query = db.query(AuditLog)
|
||||||
|
|
||||||
|
if module_name:
|
||||||
|
query = query.filter(AuditLog.module_name == module_name)
|
||||||
|
if action_type:
|
||||||
|
query = query.filter(AuditLog.action_type == action_type.upper())
|
||||||
|
if performed_by_email:
|
||||||
|
query = query.filter(
|
||||||
|
AuditLog.performed_by_email.ilike(f"%{performed_by_email}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
total = query.count()
|
||||||
|
items = (
|
||||||
|
query.order_by(AuditLog.created_at.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
return AuditLogListResponse(
|
||||||
|
items=items,
|
||||||
|
total=total,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
@@ -1,13 +1,35 @@
|
|||||||
from fastapi import APIRouter, Depends
|
import uuid as uuid_module
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config.database import get_db
|
from app.config.database import get_db
|
||||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||||
from app.schemas.auth.module_environment_schema import EnvironmentCreate, EnvironmentUpdate, EnvironmentResponse
|
|
||||||
from app.controllers.auth.module_environment_controller import ModuleEnvironmentController
|
from app.controllers.auth.module_environment_controller import ModuleEnvironmentController
|
||||||
|
from app.models.auth.module_environment_model import ModuleEnvironment
|
||||||
|
from app.schemas.auth.module_environment_schema import EnvironmentCreate, EnvironmentResponse, EnvironmentUpdate
|
||||||
|
from app.services.auth.module_service import ModuleService
|
||||||
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
def _get_env_snapshot(db: Session, module_id: str, env_id: str) -> dict:
|
||||||
|
"""Helper to capture state before changes for the Audit Log."""
|
||||||
|
try:
|
||||||
|
env = db.query(ModuleEnvironment).filter(
|
||||||
|
ModuleEnvironment.id == uuid_module.UUID(env_id),
|
||||||
|
ModuleEnvironment.module_id == uuid_module.UUID(module_id)
|
||||||
|
).first()
|
||||||
|
if not env: return {}
|
||||||
|
return {
|
||||||
|
"slug": env.slug,
|
||||||
|
"backend_base_url": env.backend_base_url,
|
||||||
|
"is_default": env.is_default,
|
||||||
|
"is_active": env.is_active
|
||||||
|
}
|
||||||
|
except: return {}
|
||||||
|
|
||||||
@router.get("/{module_id}/environments", response_model=List[EnvironmentResponse])
|
@router.get("/{module_id}/environments", response_model=List[EnvironmentResponse])
|
||||||
def list_environments(
|
def list_environments(
|
||||||
module_id: str,
|
module_id: str,
|
||||||
@@ -17,18 +39,35 @@ def list_environments(
|
|||||||
):
|
):
|
||||||
return ModuleEnvironmentController.list_environments(db, module_id)
|
return ModuleEnvironmentController.list_environments(db, module_id)
|
||||||
|
|
||||||
@router.post("/{module_id}/environments", response_model=EnvironmentResponse)
|
@router.post("/{module_id}/environments", response_model=EnvironmentResponse, status_code=201)
|
||||||
def create_environment(
|
def create_environment(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
env_data: EnvironmentCreate,
|
env_data: EnvironmentCreate,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleEnvironmentController.create_environment(db, module_id, env_data)
|
module = ModuleService.get_module(db, module_id)
|
||||||
|
result = ModuleEnvironmentController.create_environment(db, module_id, env_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Module Environments",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=f"{module.module_name} / {result.slug}",
|
||||||
|
description=f"Environment '{result.slug}' created for '{module.module_name}'",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values=env_data.model_dump(exclude={"trust_credentials"})
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.put("/{module_id}/environments/{env_id}", response_model=EnvironmentResponse)
|
@router.put("/{module_id}/environments/{env_id}", response_model=EnvironmentResponse)
|
||||||
def update_environment(
|
def update_environment(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
env_id: str,
|
env_id: str,
|
||||||
env_data: EnvironmentUpdate,
|
env_data: EnvironmentUpdate,
|
||||||
@@ -36,24 +75,76 @@ def update_environment(
|
|||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleEnvironmentController.update_environment(db, module_id, env_id, env_data)
|
module = ModuleService.get_module(db, module_id)
|
||||||
|
old_values = _get_env_snapshot(db, module_id, env_id)
|
||||||
|
|
||||||
|
result = ModuleEnvironmentController.update_environment(db, module_id, env_id, env_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Module Environments",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=env_id,
|
||||||
|
entity_name=f"{module.module_name} / {result.slug}",
|
||||||
|
description=f"Environment '{result.slug}' updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=env_data.model_dump(exclude_unset=True, exclude={"trust_credentials"})
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.patch("/{module_id}/environments/{env_id}/default")
|
@router.patch("/{module_id}/environments/{env_id}/default")
|
||||||
def set_default_environment(
|
def set_default_environment(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
env_id: str,
|
env_id: str,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleEnvironmentController.set_default_environment(db, module_id, env_id)
|
module = ModuleService.get_module(db, module_id)
|
||||||
|
result = ModuleEnvironmentController.set_default_environment(db, module_id, env_id)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Module Environments",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=env_id,
|
||||||
|
entity_name=f"{module.module_name}",
|
||||||
|
description=f"Set default environment for {module.module_name}",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values={"is_default": True}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/{module_id}/environments/{env_id}")
|
@router.delete("/{module_id}/environments/{env_id}")
|
||||||
def delete_environment(
|
def delete_environment(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
env_id: str,
|
env_id: str,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleEnvironmentController.delete_environment(db, module_id, env_id)
|
module = ModuleService.get_module(db, module_id)
|
||||||
|
snapshot = _get_env_snapshot(db, module_id, env_id)
|
||||||
|
|
||||||
|
result = ModuleEnvironmentController.delete_environment(db, module_id, env_id)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Module Environments",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=env_id,
|
||||||
|
entity_name=f"{module.module_name} / {snapshot.get('slug')}",
|
||||||
|
description=f"Environment deleted from {module.module_name}",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=snapshot
|
||||||
|
)
|
||||||
|
return result
|
||||||
+71
-28
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
from app.models.auth.module_model import Module
|
from app.models.auth.module_model import Module
|
||||||
@@ -10,6 +10,8 @@ from app.services.auth.module_permission_service import ModulePermissionService
|
|||||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||||
from app.schemas.auth.module_schema import ModuleCreate, ModuleUpdate, ModuleResponse
|
from app.schemas.auth.module_schema import ModuleCreate, ModuleUpdate, ModuleResponse
|
||||||
from app.controllers.auth.module_controller import ModuleController
|
from app.controllers.auth.module_controller import ModuleController
|
||||||
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -21,14 +23,30 @@ def list_modules(
|
|||||||
):
|
):
|
||||||
return ModuleController.list_modules(db)
|
return ModuleController.list_modules(db)
|
||||||
|
|
||||||
@router.post("/", response_model=ModuleResponse)
|
@router.post("/", response_model=ModuleResponse, status_code=201)
|
||||||
def create_module(
|
def create_module(
|
||||||
|
request: Request,
|
||||||
module_data: ModuleCreate,
|
module_data: ModuleCreate,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleController.create_module(db, module_data)
|
result = ModuleController.create_module(db, module_data)
|
||||||
|
|
||||||
|
# Professional Audit Logging
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Modules",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=result.module_name,
|
||||||
|
description=f"Module '{result.module_name}' created",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values=module_data.model_dump()
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/{module_id}", response_model=ModuleResponse)
|
@router.get("/{module_id}", response_model=ModuleResponse)
|
||||||
def get_module(
|
def get_module(
|
||||||
@@ -41,48 +59,72 @@ def get_module(
|
|||||||
|
|
||||||
@router.put("/{module_id}", response_model=ModuleResponse)
|
@router.put("/{module_id}", response_model=ModuleResponse)
|
||||||
def update_module(
|
def update_module(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
module_data: ModuleUpdate,
|
module_data: ModuleUpdate,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleController.update_module(db, module_id, module_data)
|
# Snapshot before update
|
||||||
|
existing = ModuleController.get_module(db, module_id)
|
||||||
|
old_values = {"name": existing.module_name, "status": existing.status}
|
||||||
|
|
||||||
|
result = ModuleController.update_module(db, module_id, module_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Modules",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=module_id,
|
||||||
|
entity_name=result.module_name,
|
||||||
|
description=f"Module '{result.module_name}' updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=module_data.model_dump(exclude_unset=True)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/{module_id}")
|
@router.delete("/{module_id}")
|
||||||
def delete_module(
|
def delete_module(
|
||||||
|
request: Request,
|
||||||
module_id: str,
|
module_id: str,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return ModuleController.delete_module(db, module_id)
|
# Capture name for the log before it's deleted
|
||||||
|
existing = ModuleController.get_module(db, module_id)
|
||||||
|
module_name = existing.module_name
|
||||||
|
|
||||||
@router.get("/{module_id}/permissions", response_model=List[dict])
|
result = ModuleController.delete_module(db, module_id)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Modules",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=module_id,
|
||||||
|
entity_name=module_name,
|
||||||
|
description=f"Module '{module_name}' deleted",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ── Permission Sync Routes ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/{module_id}/permissions")
|
||||||
def get_module_permissions(
|
def get_module_permissions(
|
||||||
module_id: str,
|
module_id: str,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("modules.view")),
|
_: bool = Depends(require_access("modules.view")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
# Now using the Service method you just shared!
|
||||||
module = db.query(Module).filter(Module.id == uuid.UUID(module_id)).first()
|
return ModulePermissionService.get_module_permissions(db, module_id)
|
||||||
if not module:
|
|
||||||
raise HTTPException(status_code=404, detail="Module not found")
|
|
||||||
|
|
||||||
permissions = db.query(ModuleAccess).filter(
|
|
||||||
ModuleAccess.module_id == module.id
|
|
||||||
).all()
|
|
||||||
|
|
||||||
return [{
|
|
||||||
"id": str(p.id),
|
|
||||||
"access_code": p.access_code,
|
|
||||||
"name": p.name,
|
|
||||||
"category": p.category,
|
|
||||||
"parent_id": str(p.parent_id) if p.parent_id else None,
|
|
||||||
"scope": "module",
|
|
||||||
"module_id": str(p.module_id)
|
|
||||||
} for p in permissions]
|
|
||||||
|
|
||||||
@router.post("/{module_id}/permissions/sync")
|
@router.post("/{module_id}/permissions/sync")
|
||||||
def sync_module_permissions(
|
def sync_module_permissions(
|
||||||
@@ -91,11 +133,12 @@ def sync_module_permissions(
|
|||||||
_: bool = Depends(require_access("modules.manage")),
|
_: bool = Depends(require_access("modules.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = ModulePermissionService.sync_permissions(db, module_id)
|
return ModulePermissionService.sync_permissions(db, module_id)
|
||||||
return result
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
raise e
|
raise e
|
||||||
raise HTTPException(status_code=500, detail=f"Permission sync failed: {str(e)}")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Permission sync failed: {str(e)}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, status, HTTPException, Request, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -6,8 +6,10 @@ from app.config.database import get_db
|
|||||||
from app.middleware.auth_middleware import get_current_user, require_access, User
|
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||||
from app.schemas.auth.tenant_module_schema import TenantModuleCreate, TenantModuleUpdate, TenantModuleResponse
|
from app.schemas.auth.tenant_module_schema import TenantModuleCreate, TenantModuleUpdate, TenantModuleResponse
|
||||||
from app.controllers.auth.tenant_module_controller import TenantModuleController
|
from app.controllers.auth.tenant_module_controller import TenantModuleController
|
||||||
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.get("/{tenant_id}/modules", response_model=List[TenantModuleResponse])
|
@router.get("/{tenant_id}/modules", response_model=List[TenantModuleResponse])
|
||||||
def list_tenant_modules(
|
def list_tenant_modules(
|
||||||
@@ -20,16 +22,32 @@ def list_tenant_modules(
|
|||||||
|
|
||||||
@router.post("/{tenant_id}/modules", response_model=TenantModuleResponse)
|
@router.post("/{tenant_id}/modules", response_model=TenantModuleResponse)
|
||||||
def assign_module_to_tenant(
|
def assign_module_to_tenant(
|
||||||
|
request: Request,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
assignment_data: TenantModuleCreate,
|
assignment_data: TenantModuleCreate,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("tenants.manage")),
|
_: bool = Depends(require_access("tenants.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return TenantModuleController.assign_module(db, tenant_id, assignment_data)
|
result = TenantModuleController.assign_module(db, tenant_id, assignment_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenant Modules",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=f"Tenant {tenant_id} / Module {assignment_data.module_id}",
|
||||||
|
description=f"Module assigned to tenant {tenant_id}",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values=assignment_data.model_dump(mode='json')
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.put("/{tenant_id}/modules/{tenant_module_id}", response_model=TenantModuleResponse)
|
@router.put("/{tenant_id}/modules/{tenant_module_id}", response_model=TenantModuleResponse)
|
||||||
def update_tenant_module(
|
def update_tenant_module(
|
||||||
|
request: Request,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
tenant_module_id: str,
|
tenant_module_id: str,
|
||||||
update_data: TenantModuleUpdate,
|
update_data: TenantModuleUpdate,
|
||||||
@@ -37,14 +55,42 @@ def update_tenant_module(
|
|||||||
_: bool = Depends(require_access("tenants.manage")),
|
_: bool = Depends(require_access("tenants.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return TenantModuleController.update_assignment(db, tenant_id, tenant_module_id, update_data)
|
result = TenantModuleController.update_assignment(db, tenant_id, tenant_module_id, update_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenant Modules",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=tenant_module_id,
|
||||||
|
entity_name=f"Tenant {tenant_id} / Module assignment",
|
||||||
|
description=f"Tenant module assignment updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values=update_data.model_dump(exclude_unset=True)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/{tenant_id}/modules/{tenant_module_id}")
|
@router.delete("/{tenant_id}/modules/{tenant_module_id}")
|
||||||
def remove_module_from_tenant(
|
def remove_module_from_tenant(
|
||||||
|
request: Request,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
tenant_module_id: str,
|
tenant_module_id: str,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_: bool = Depends(require_access("tenants.manage")),
|
_: bool = Depends(require_access("tenants.manage")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
return TenantModuleController.remove_assignment(db, tenant_id, tenant_module_id)
|
result = TenantModuleController.remove_assignment(db, tenant_id, tenant_module_id)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenant Modules",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=tenant_module_id,
|
||||||
|
entity_name=f"Tenant {tenant_id} / Module assignment",
|
||||||
|
description=f"Tenant module assignment deleted",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
)
|
||||||
|
return result
|
||||||
+65
-4
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, status, Query
|
from fastapi import APIRouter, Depends, status, Query, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
@@ -13,20 +13,40 @@ from app.schemas.auth.role_schema import (
|
|||||||
)
|
)
|
||||||
from app.middleware.auth_middleware import get_current_user, require_access
|
from app.middleware.auth_middleware import get_current_user, require_access
|
||||||
from app.models.auth.user_model import User
|
from app.models.auth.user_model import User
|
||||||
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/create", response_model=RoleResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/create", response_model=RoleResponse, status_code=status.HTTP_201_CREATED)
|
||||||
def create_role(
|
def create_role(
|
||||||
|
request: Request,
|
||||||
role_data: RoleCreate,
|
role_data: RoleCreate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_=Depends(require_access("admin.role.create")),
|
_=Depends(require_access("admin.role.create")),
|
||||||
):
|
):
|
||||||
return RoleController.create_role(
|
result = RoleController.create_role(
|
||||||
db, role_data, current_user.tenant_id if current_user.tenant_id else None
|
db, role_data, current_user.tenant_id if current_user.tenant_id else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Roles",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=result.role_name,
|
||||||
|
description=f"Role '{result.role_name}' created",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values={
|
||||||
|
"role_name": result.role_name,
|
||||||
|
"tenant_id": str(result.tenant_id) if result.tenant_id else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/get", response_model=List[RoleResponse])
|
@router.get("/get", response_model=List[RoleResponse])
|
||||||
def get_all_roles(
|
def get_all_roles(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -48,26 +68,67 @@ def get_role(
|
|||||||
|
|
||||||
@router.put("/update/{role_id}", response_model=RoleResponse)
|
@router.put("/update/{role_id}", response_model=RoleResponse)
|
||||||
def update_role(
|
def update_role(
|
||||||
|
request: Request,
|
||||||
role_id: uuid.UUID,
|
role_id: uuid.UUID,
|
||||||
role_data: RoleUpdate,
|
role_data: RoleUpdate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_=Depends(require_access("admin.role.update")),
|
_=Depends(require_access("admin.role.update")),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
existing = RoleController.get_role_by_id(db, role_id)
|
||||||
|
old_values = {"role_name": existing.role_name}
|
||||||
|
|
||||||
is_superadmin = current_user.tenant_id is None
|
is_superadmin = current_user.tenant_id is None
|
||||||
return RoleController.update_role(
|
result = RoleController.update_role(
|
||||||
db, role_id, role_data, is_superadmin=is_superadmin
|
db, role_id, role_data, is_superadmin=is_superadmin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
new_values = role_data.model_dump(mode='json', exclude_unset=True)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Roles",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=str(role_id),
|
||||||
|
entity_name=result.role_name,
|
||||||
|
description=f"Role '{result.role_name}' updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=new_values
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/delete/{role_id}")
|
@router.delete("/delete/{role_id}")
|
||||||
def delete_role(
|
def delete_role(
|
||||||
|
request: Request,
|
||||||
role_id: uuid.UUID,
|
role_id: uuid.UUID,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_=Depends(require_access("admin.role.delete")),
|
_=Depends(require_access("admin.role.delete")),
|
||||||
):
|
):
|
||||||
|
|
||||||
|
existing = RoleController.get_role_by_id(db, role_id)
|
||||||
|
role_name = existing.role_name
|
||||||
|
|
||||||
is_superadmin = current_user.tenant_id is None
|
is_superadmin = current_user.tenant_id is None
|
||||||
return RoleController.delete_role(db, role_id, is_superadmin=is_superadmin)
|
result = RoleController.delete_role(db, role_id, is_superadmin=is_superadmin)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Roles",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=str(role_id),
|
||||||
|
entity_name=role_name,
|
||||||
|
description=f"Role '{result.role_name}' deleted",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values={"role_name": role_name}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/list", response_model=RolePaginatedResponse)
|
@router.get("/list", response_model=RolePaginatedResponse)
|
||||||
def list_roles(
|
def list_roles(
|
||||||
|
|||||||
+79
-21
@@ -1,24 +1,41 @@
|
|||||||
from fastapi import APIRouter, Depends, status, HTTPException, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from typing import List, Optional
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import List, Optional
|
||||||
|
from fastapi import APIRouter, Depends, status, HTTPException, Query, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.config.database import get_db
|
from app.config.database import get_db
|
||||||
from app.controllers.auth.tenant_controller import TenantController
|
from app.controllers.auth.tenant_controller import TenantController
|
||||||
from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantResponse, TenantPaginatedResponse
|
from app.schemas.auth.tenant_schema import TenantCreate, TenantUpdate, TenantResponse, TenantPaginatedResponse
|
||||||
from app.middleware.auth_middleware import get_current_user, require_access
|
from app.middleware.auth_middleware import get_current_user, require_access, User
|
||||||
from app.middleware.tenant_middleware import get_tenant_id_from_user
|
from app.middleware.tenant_middleware import get_tenant_id_from_user
|
||||||
from app.models.auth.user_model import User
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/create", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/create", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
|
||||||
def create_tenant(
|
def create_tenant(
|
||||||
|
request: Request,
|
||||||
tenant_data: TenantCreate,
|
tenant_data: TenantCreate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("superadmin.tenant.create"))
|
_ = Depends(require_access("superadmin.tenant.create"))
|
||||||
):
|
):
|
||||||
return TenantController.create_tenant(db, tenant_data)
|
result = TenantController.create_tenant(db, tenant_data)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenants",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=result.tenant_name,
|
||||||
|
description=f"Tenant '{result.tenant_name}' onboarded",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values=tenant_data.model_dump(mode='json')
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/get", response_model=List[TenantResponse])
|
@router.get("/get", response_model=List[TenantResponse])
|
||||||
def get_all_tenants(
|
def get_all_tenants(
|
||||||
@@ -26,17 +43,9 @@ def get_all_tenants(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("superadmin.tenant.read"))
|
_ = Depends(require_access("superadmin.tenant.read"))
|
||||||
):
|
):
|
||||||
|
# READ actions are typically not logged to avoid DB bloat
|
||||||
return TenantController.get_all_tenants(db)
|
return TenantController.get_all_tenants(db)
|
||||||
|
|
||||||
@router.get("/get/{tenant_id}", response_model=TenantResponse)
|
|
||||||
def get_tenant(
|
|
||||||
tenant_id: uuid.UUID,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
_ = Depends(require_access("superadmin.tenant.read"))
|
|
||||||
):
|
|
||||||
return TenantController.get_tenant_by_id(db, tenant_id)
|
|
||||||
|
|
||||||
@router.get("/me", response_model=TenantResponse)
|
@router.get("/me", response_model=TenantResponse)
|
||||||
def get_my_tenant(
|
def get_my_tenant(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -44,30 +53,79 @@ def get_my_tenant(
|
|||||||
):
|
):
|
||||||
tenant_id = get_tenant_id_from_user(current_user)
|
tenant_id = get_tenant_id_from_user(current_user)
|
||||||
if not tenant_id:
|
if not tenant_id:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Tenant not found"
|
|
||||||
)
|
|
||||||
return TenantController.get_tenant_by_id(db, tenant_id)
|
return TenantController.get_tenant_by_id(db, tenant_id)
|
||||||
|
|
||||||
@router.put("/update/{tenant_id}", response_model=TenantResponse)
|
@router.put("/update/{tenant_id}", response_model=TenantResponse)
|
||||||
def update_tenant(
|
def update_tenant(
|
||||||
|
request: Request,
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
tenant_data: TenantUpdate,
|
tenant_data: TenantUpdate,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("superadmin.tenant.update"))
|
_ = Depends(require_access("superadmin.tenant.update"))
|
||||||
):
|
):
|
||||||
return TenantController.update_tenant(db, tenant_id, tenant_data)
|
# 1. Get snapshot BEFORE update for Audit Log
|
||||||
|
existing = TenantController.get_tenant_by_id(db, tenant_id)
|
||||||
|
old_snapshot = {
|
||||||
|
"tenant_name": existing.tenant_name,
|
||||||
|
"tenant_domain": existing.tenant_domain,
|
||||||
|
"is_active": existing.is_active
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Perform update
|
||||||
|
result = TenantController.update_tenant(db, tenant_id, tenant_data)
|
||||||
|
|
||||||
|
# 3. Prepare new values and indentify deltas
|
||||||
|
new_snapshot = tenant_data.model_dump(mode='json', exclude_unset=True)
|
||||||
|
|
||||||
|
old_values = {k: old_snapshot[k] for k in new_snapshot if k in old_snapshot and old_snapshot[k] != new_snapshot[k]}
|
||||||
|
new_values = {k: new_snapshot[k] for k in old_values}
|
||||||
|
|
||||||
|
# 4. Log the change
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenants",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=str(tenant_id),
|
||||||
|
entity_name=result.tenant_name,
|
||||||
|
description=f"Tenant '{result.tenant_name}' updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=new_values
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/delete/{tenant_id}")
|
@router.delete("/delete/{tenant_id}")
|
||||||
def delete_tenant(
|
def delete_tenant(
|
||||||
|
request: Request,
|
||||||
tenant_id: uuid.UUID,
|
tenant_id: uuid.UUID,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("superadmin.tenant.delete"))
|
_ = Depends(require_access("superadmin.tenant.delete"))
|
||||||
):
|
):
|
||||||
return TenantController.delete_tenant(db, tenant_id)
|
# 1. Get snapshot before deletion
|
||||||
|
existing = TenantController.get_tenant_by_id(db, tenant_id)
|
||||||
|
|
||||||
|
# 2. Perform deletion
|
||||||
|
result = TenantController.delete_tenant(db, tenant_id)
|
||||||
|
|
||||||
|
# 3. Log the deletion
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Tenants",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=str(tenant_id),
|
||||||
|
entity_name=existing.tenant_name,
|
||||||
|
description=f"Tenant '{existing.tenant_name}' removed",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values={"tenant_name": existing.tenant_name, "domain": existing.tenant_domain}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/list", response_model=TenantPaginatedResponse)
|
@router.get("/list", response_model=TenantPaginatedResponse)
|
||||||
def list_tenants(
|
def list_tenants(
|
||||||
|
|||||||
+64
-5
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, status, Query, BackgroundTasks
|
from fastapi import APIRouter, Depends, status, Query, BackgroundTasks, Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import uuid
|
import uuid
|
||||||
@@ -7,18 +7,38 @@ from app.controllers.auth.user_controller import UserController
|
|||||||
from app.schemas.auth.user_schema import UserCreate, UserUpdate, UserResponse, UserPaginatedResponse
|
from app.schemas.auth.user_schema import UserCreate, UserUpdate, UserResponse, UserPaginatedResponse
|
||||||
from app.middleware.auth_middleware import get_current_user, require_access
|
from app.middleware.auth_middleware import get_current_user, require_access
|
||||||
from app.models.auth.user_model import User
|
from app.models.auth.user_model import User
|
||||||
|
from app.services.system.audit_log_service import AuditLogService
|
||||||
|
from app.helper.helpers import get_client_ip
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("/create", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/create", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||||
def create_user(
|
def create_user(
|
||||||
|
request: Request,
|
||||||
user_data: UserCreate,
|
user_data: UserCreate,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("admin.user.create"))
|
_ = Depends(require_access("admin.user.create"))
|
||||||
):
|
):
|
||||||
return UserController.create_user(db, user_data, current_user, background_tasks)
|
result = UserController.create_user(db, user_data, current_user, background_tasks)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Users",
|
||||||
|
action_type="CREATE",
|
||||||
|
entity_id=str(result.id),
|
||||||
|
entity_name=result.email,
|
||||||
|
description=f"User '{result.email}' created",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
new_values={
|
||||||
|
"email": result.email,
|
||||||
|
"tenant_id": str(result.tenant_id) if result.tenant_id else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/get", response_model=List[UserResponse])
|
@router.get("/get", response_model=List[UserResponse])
|
||||||
def get_all_users(
|
def get_all_users(
|
||||||
@@ -39,6 +59,7 @@ def get_user(
|
|||||||
|
|
||||||
@router.put("/update/{user_id}", response_model=UserResponse)
|
@router.put("/update/{user_id}", response_model=UserResponse)
|
||||||
def update_user(
|
def update_user(
|
||||||
|
request: Request,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
user_data: UserUpdate,
|
user_data: UserUpdate,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
@@ -46,16 +67,54 @@ def update_user(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("admin.user.update"))
|
_ = Depends(require_access("admin.user.update"))
|
||||||
):
|
):
|
||||||
return UserController.update_user(db, user_id, user_data, current_user, background_tasks)
|
|
||||||
|
existing = UserController.get_user_by_id(db, user_id, current_user)
|
||||||
|
old_values = {"email": existing.email}
|
||||||
|
|
||||||
|
result = UserController.update_user(db, user_id, user_data, current_user, background_tasks)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Users",
|
||||||
|
action_type="UPDATE",
|
||||||
|
entity_id=str(user_id),
|
||||||
|
entity_name=result.email,
|
||||||
|
description=f"User '{result.email}' updated",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=user_data.model_dump(mode='json', exclude_unset=True)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/delete/{user_id}")
|
@router.delete("/delete/{user_id}")
|
||||||
def delete_user(
|
def delete_user(
|
||||||
|
request: Request,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
_ = Depends(require_access("admin.user.delete"))
|
_ = Depends(require_access("admin.user.delete"))
|
||||||
):
|
):
|
||||||
return UserController.delete_user(db, user_id, current_user)
|
|
||||||
|
existing = UserController.get_user_by_id(db, user_id, current_user)
|
||||||
|
user_email = existing.email
|
||||||
|
|
||||||
|
result = UserController.delete_user(db, user_id, current_user)
|
||||||
|
|
||||||
|
AuditLogService.log(
|
||||||
|
db=db,
|
||||||
|
module_name="Users",
|
||||||
|
action_type="DELETE",
|
||||||
|
entity_id=str(user_id),
|
||||||
|
entity_name=user_email,
|
||||||
|
description=f"User '{user_email}' deleted",
|
||||||
|
performed_by_id=str(current_user.id),
|
||||||
|
performed_by_email=current_user.email,
|
||||||
|
ip_address=_get_client_ip(request),
|
||||||
|
old_values={"email": user_email}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@router.get("/list", response_model=UserPaginatedResponse)
|
@router.get("/list", response_model=UserPaginatedResponse)
|
||||||
def list_users(
|
def list_users(
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class AuditLogResponse(BaseModel):
|
||||||
|
id: UUID
|
||||||
|
module_name: str
|
||||||
|
action_type: str
|
||||||
|
entity_id: Optional[str] = None
|
||||||
|
entity_name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
performed_by_email: Optional[str] = None
|
||||||
|
ip_address: Optional[str] = None
|
||||||
|
old_values: Optional[Dict[str, Any]] = None
|
||||||
|
new_values: Optional[Dict[str, Any]] = None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class AuditLogListResponse(BaseModel):
|
||||||
|
items: List[AuditLogResponse]
|
||||||
|
total: int
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.system.audit_log import AuditLog
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogService:
|
||||||
|
@staticmethod
|
||||||
|
def log(
|
||||||
|
db: Session,
|
||||||
|
module_name: str,
|
||||||
|
action_type: str,
|
||||||
|
description: str,
|
||||||
|
entity_id: Optional[str] = None,
|
||||||
|
entity_name: Optional[str] = None,
|
||||||
|
performed_by_id: Optional[str] = None,
|
||||||
|
performed_by_email: Optional[str] = None,
|
||||||
|
ip_address: Optional[str] = None,
|
||||||
|
old_values: Optional[Dict[str, Any]] = None,
|
||||||
|
new_values: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Write an audit log entry.
|
||||||
|
|
||||||
|
Failures are silently swallowed and logged — they must NEVER break
|
||||||
|
the main operation that triggered them.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
entry = AuditLog(
|
||||||
|
module_name=module_name,
|
||||||
|
action_type=action_type,
|
||||||
|
entity_id=entity_id,
|
||||||
|
entity_name=entity_name,
|
||||||
|
description=description,
|
||||||
|
performed_by_id=(
|
||||||
|
uuid.UUID(performed_by_id) if performed_by_id else None
|
||||||
|
),
|
||||||
|
performed_by_email=performed_by_email,
|
||||||
|
ip_address=ip_address,
|
||||||
|
old_values=old_values,
|
||||||
|
new_values=new_values,
|
||||||
|
)
|
||||||
|
db.add(entry)
|
||||||
|
db.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
db.rollback()
|
||||||
|
logger.error("AuditLogService.log failed: %s", exc, exc_info=True)
|
||||||
@@ -64,6 +64,7 @@ PREDEFINED_ACCESSES = [
|
|||||||
("admin.user.update", "Administration", "Allow access to update users", None),
|
("admin.user.update", "Administration", "Allow access to update users", None),
|
||||||
("admin.user.delete", "Administration", "Allow access to delete users", None),
|
("admin.user.delete", "Administration", "Allow access to delete users", None),
|
||||||
("admin.access.read", "Administration", "Allow access to view accesses", None),
|
("admin.access.read", "Administration", "Allow access to view accesses", None),
|
||||||
|
("admin.logs.read", "Administration", "Allow access to view system audit logs", None),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user