finishing QDRO section
This commit is contained in:
174
app/api/admin.py
174
app/api/admin.py
@@ -73,6 +73,7 @@ class UserCreate(BaseModel):
|
||||
last_name: Optional[str] = None
|
||||
is_admin: bool = False
|
||||
is_active: bool = True
|
||||
is_approver: bool = False
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""Update user information"""
|
||||
@@ -82,6 +83,7 @@ class UserUpdate(BaseModel):
|
||||
last_name: Optional[str] = None
|
||||
is_admin: Optional[bool] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_approver: Optional[bool] = None
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""User response model"""
|
||||
@@ -92,6 +94,7 @@ class UserResponse(BaseModel):
|
||||
last_name: Optional[str]
|
||||
is_admin: bool
|
||||
is_active: bool
|
||||
is_approver: bool
|
||||
last_login: Optional[datetime]
|
||||
created_at: Optional[datetime]
|
||||
updated_at: Optional[datetime]
|
||||
@@ -103,6 +106,44 @@ class PasswordReset(BaseModel):
|
||||
new_password: str = Field(..., min_length=6)
|
||||
confirm_password: str = Field(..., min_length=6)
|
||||
|
||||
|
||||
# Approver management
|
||||
class ApproverToggle(BaseModel):
|
||||
is_approver: bool
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/approver", response_model=UserResponse)
|
||||
async def set_user_approver(
|
||||
user_id: int,
|
||||
payload: ApproverToggle,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
"""Admin-only toggle for user approver role with audit logging."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
previous = bool(getattr(user, "is_approver", False))
|
||||
user.is_approver = bool(payload.is_approver)
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
if previous != user.is_approver:
|
||||
try:
|
||||
audit_service.log_user_action(
|
||||
db=db,
|
||||
action="UPDATE",
|
||||
target_user=user,
|
||||
acting_user=current_user,
|
||||
changes={"is_approver": {"from": previous, "to": user.is_approver}},
|
||||
request=request,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return user
|
||||
|
||||
class SystemSetting(BaseModel):
|
||||
"""System setting model"""
|
||||
setting_key: str
|
||||
@@ -115,6 +156,56 @@ class SettingUpdate(BaseModel):
|
||||
setting_value: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
# ------------------------------
|
||||
# QDRO Notification Route Models
|
||||
# ------------------------------
|
||||
|
||||
class NotificationRoute(BaseModel):
|
||||
scope: str = Field(description="file or plan")
|
||||
identifier: str = Field(description="file_no when scope=file, plan_id when scope=plan")
|
||||
email_to: Optional[str] = None
|
||||
webhook_url: Optional[str] = None
|
||||
webhook_secret: Optional[str] = None
|
||||
|
||||
|
||||
def _route_keys(scope: str, identifier: str) -> dict[str, str]:
|
||||
if scope not in {"file", "plan"}:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid scope; expected 'file' or 'plan'")
|
||||
return {
|
||||
"email": f"notifications.qdro.email.to.{scope}.{identifier}",
|
||||
"webhook_url": f"notifications.qdro.webhook.url.{scope}.{identifier}",
|
||||
"webhook_secret": f"notifications.qdro.webhook.secret.{scope}.{identifier}",
|
||||
}
|
||||
|
||||
|
||||
def _get_setting(db: Session, key: str) -> Optional[str]:
|
||||
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
|
||||
return row.setting_value if row else None
|
||||
|
||||
|
||||
def _upsert_setting(db: Session, key: str, value: Optional[str]) -> None:
|
||||
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
|
||||
if value is None or value == "":
|
||||
if row:
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return
|
||||
if row:
|
||||
row.setting_value = value
|
||||
db.commit()
|
||||
return
|
||||
row = SystemSetup(setting_key=key, setting_value=value, description=f"Auto: {key}")
|
||||
db.add(row)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _delete_setting(db: Session, key: str) -> None:
|
||||
row = db.query(SystemSetup).filter(SystemSetup.setting_key == key).first()
|
||||
if row:
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
|
||||
class AuditLogEntry(BaseModel):
|
||||
"""Audit log entry"""
|
||||
id: int
|
||||
@@ -726,6 +817,7 @@ async def create_user(
|
||||
hashed_password=hashed_password,
|
||||
is_admin=user_data.is_admin,
|
||||
is_active=user_data.is_active,
|
||||
is_approver=user_data.is_approver,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
@@ -807,7 +899,8 @@ async def update_user(
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"is_admin": user.is_admin,
|
||||
"is_active": user.is_active
|
||||
"is_active": user.is_active,
|
||||
"is_approver": user.is_approver,
|
||||
}
|
||||
|
||||
# Update user fields
|
||||
@@ -1063,6 +1156,85 @@ async def delete_setting(
|
||||
return {"message": "Setting deleted successfully"}
|
||||
|
||||
|
||||
# ------------------------------
|
||||
# QDRO Notification Routing CRUD
|
||||
# ------------------------------
|
||||
|
||||
@router.get("/qdro/notification-routes")
|
||||
async def list_qdro_notification_routes(
|
||||
scope: Optional[str] = Query(None, description="Optional filter: file or plan"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
q = db.query(SystemSetup).filter(SystemSetup.setting_key.like("notifications.qdro.%"))
|
||||
rows = q.all()
|
||||
# Build map of identifier -> route
|
||||
route_map: dict[tuple[str, str], dict[str, Optional[str]]] = {}
|
||||
for r in rows:
|
||||
key = r.setting_key
|
||||
parts = key.split(".")
|
||||
# notifications qdro <type> <to|url|secret> <scope> <identifier>
|
||||
if len(parts) < 7:
|
||||
# Example: notifications.qdro.email.to.file.{id}
|
||||
# parts: [notifications, qdro, email, to, file, {id}]
|
||||
pass
|
||||
if len(parts) >= 6 and parts[0] == "notifications" and parts[1] == "qdro":
|
||||
typ = parts[2]
|
||||
field = parts[3]
|
||||
sc = parts[4]
|
||||
ident = ".".join(parts[5:]) # support dots in identifiers just in case
|
||||
if scope and sc != scope:
|
||||
continue
|
||||
route = route_map.setdefault((sc, ident), {"scope": sc, "identifier": ident, "email_to": None, "webhook_url": None, "webhook_secret": None})
|
||||
if typ == "email" and field == "to":
|
||||
route["email_to"] = r.setting_value
|
||||
elif typ == "webhook" and field == "url":
|
||||
route["webhook_url"] = r.setting_value
|
||||
elif typ == "webhook" and field == "secret":
|
||||
route["webhook_secret"] = r.setting_value
|
||||
# Format list
|
||||
out = [
|
||||
{
|
||||
"scope": sc,
|
||||
"identifier": ident,
|
||||
"email_to": data.get("email_to"),
|
||||
"webhook_url": data.get("webhook_url"),
|
||||
"webhook_secret": data.get("webhook_secret"),
|
||||
}
|
||||
for (sc, ident), data in route_map.items()
|
||||
]
|
||||
return {"items": out, "total": len(out)}
|
||||
|
||||
|
||||
@router.post("/qdro/notification-routes")
|
||||
async def upsert_qdro_notification_route(
|
||||
payload: NotificationRoute,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
keys = _route_keys(payload.scope, payload.identifier)
|
||||
_upsert_setting(db, keys["email"], payload.email_to)
|
||||
_upsert_setting(db, keys["webhook_url"], payload.webhook_url)
|
||||
# Preserve existing secret unless a new value is provided
|
||||
if payload.webhook_secret is not None and payload.webhook_secret != "":
|
||||
_upsert_setting(db, keys["webhook_secret"], payload.webhook_secret)
|
||||
return {"message": "Route saved"}
|
||||
|
||||
|
||||
@router.delete("/qdro/notification-routes/{scope}/{identifier}")
|
||||
async def delete_qdro_notification_route(
|
||||
scope: str,
|
||||
identifier: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_admin_user),
|
||||
):
|
||||
keys = _route_keys(scope, identifier)
|
||||
_delete_setting(db, keys["email"])
|
||||
_delete_setting(db, keys["webhook_url"])
|
||||
_delete_setting(db, keys["webhook_secret"])
|
||||
return {"message": "Route deleted"}
|
||||
|
||||
|
||||
# Database Maintenance and Lookup Management
|
||||
|
||||
@router.get("/lookups/tables")
|
||||
|
||||
Reference in New Issue
Block a user