Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/alembic/versions/00157630946a_add_suspended_at_to_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""add suspended_at to user

Revision ID: 00157630946a
Revises: 0bb6cf5b4577
Create Date: 2026-04-20 08:59:51.663895

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "00157630946a"
down_revision: str | Sequence[str] | None = "0bb6cf5b4577"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"user", sa.Column("suspended_at", sa.DateTime(timezone=True), nullable=True)
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "suspended_at")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""add trigram indexes for admin user search

Revision ID: 1d2f7f8c1513
Revises: 00157630946a
Create Date: 2026-04-20 15:47:24.611040

"""

from collections.abc import Sequence

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "1d2f7f8c1513"
down_revision: str | Sequence[str] | None = "00157630946a"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# gin_trgm_ops requires the pg_trgm extension; create it idempotently
# before the indexes so fresh databases can apply this migration.
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(
"ix_user_email_trgm",
"user",
["email"],
unique=False,
postgresql_using="gin",
postgresql_ops={"email": "gin_trgm_ops"},
)
op.create_index(
"ix_user_first_name_trgm",
"user",
["first_name"],
unique=False,
postgresql_using="gin",
postgresql_ops={"first_name": "gin_trgm_ops"},
)
op.create_index(
"ix_user_last_name_trgm",
"user",
["last_name"],
unique=False,
postgresql_using="gin",
postgresql_ops={"last_name": "gin_trgm_ops"},
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
"ix_user_last_name_trgm",
table_name="user",
postgresql_using="gin",
postgresql_ops={"last_name": "gin_trgm_ops"},
)
op.drop_index(
"ix_user_first_name_trgm",
table_name="user",
postgresql_using="gin",
postgresql_ops={"first_name": "gin_trgm_ops"},
)
op.drop_index(
"ix_user_email_trgm",
table_name="user",
postgresql_using="gin",
postgresql_ops={"email": "gin_trgm_ops"},
)
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion app/api/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from app.models.user import User
from app.schemas.user_activity import ActivityStatus, ActivityType, ResourceType
from app.services.user_activity_service import log_activity
from app.use_cases.log_activity import log_activity

_UNKNOWN_USER_ID = uuid.UUID(int=0)
"""Placeholder used when an unexpected failure fires before the caller is known.
Expand Down
1 change: 0 additions & 1 deletion app/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ async def get_current_user(
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=ErrorMessages.USER_NOT_FOUND
)

return user


Expand Down
3 changes: 2 additions & 1 deletion app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fastapi import APIRouter

from app.api.routes import auth, health, users
from app.api.routes import admin, auth, health, users

api_router = APIRouter()

api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(admin.router, prefix="/admin", tags=["admin"])
14 changes: 14 additions & 0 deletions app/api/routes/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Admin route aggregator.

Each resource lives in its own module (``users``, ``activities``) and is mounted
here so ``api/main.py`` only needs to import a single ``router``.
"""

from fastapi import APIRouter

from app.api.routes.admin import activities, stats, users

router = APIRouter()
router.include_router(users.router, prefix="/users")
router.include_router(activities.router)
router.include_router(stats.router)
59 changes: 59 additions & 0 deletions app/api/routes/admin/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import uuid
from datetime import datetime
from typing import Annotated

from fastapi import APIRouter, Query

from app.api.deps import CurrentSuperUser, SessionDep
from app.schemas.admin import AdminActivityListResponse
from app.schemas.user_activity import ActivityStatus, ActivityType, ResourceType
from app.services.admin.activity_service import (
list_activities_admin_service,
list_user_activities_admin_service,
)

router = APIRouter()


@router.get("/users/{user_id}/activities", response_model=AdminActivityListResponse)
async def list_user_activities(
_admin: CurrentSuperUser,
session: SessionDep,
user_id: uuid.UUID,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=200)] = 50,
) -> AdminActivityListResponse:
"""Return the activity log for a specific user."""
return await list_user_activities_admin_service(
session=session,
user_id=user_id,
skip=skip,
limit=limit,
)


@router.get("/activities", response_model=AdminActivityListResponse)
async def list_activities(
_admin: CurrentSuperUser,
session: SessionDep,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=200)] = 50,
user_id: uuid.UUID | None = None,
activity_type: ActivityType | None = None,
resource_type: ResourceType | None = None,
status_filter: Annotated[ActivityStatus | None, Query(alias="status")] = None,
date_from: datetime | None = None,
date_to: datetime | None = None,
) -> AdminActivityListResponse:
"""Return the global activity log with filters and pagination."""
return await list_activities_admin_service(
session=session,
skip=skip,
limit=limit,
user_id=user_id,
activity_type=activity_type,
resource_type=resource_type,
status_filter=status_filter,
date_from=date_from,
date_to=date_to,
)
16 changes: 16 additions & 0 deletions app/api/routes/admin/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter

from app.api.deps import CurrentSuperUser, SessionDep
from app.schemas.admin import AdminStats
from app.services.admin.stats_service import get_admin_stats_service

router = APIRouter()


@router.get("/stats", response_model=AdminStats)
async def get_stats(
_admin: CurrentSuperUser,
session: SessionDep,
) -> AdminStats:
"""Return aggregate dashboard counts in a single round-trip."""
return await get_admin_stats_service(session=session)
Loading
Loading