Files
verify/backend/app/routers/users.py
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

249 lines
6.7 KiB
Python

"""Users & Organization — team management within an org, role-scoped."""
import secrets
from typing import Optional
from fastapi import APIRouter, Body, Depends, HTTPException, status
from app.core.config import settings
from app.core.deps import require_org, require_client_roles, Principal
from app.core.security import create_token
from app.models import User, OrgMember, Plan, AuditLog
from app.models.documents import now
from app.models.enums import ClientRole
from app.services.email import send_invite_email
import csv
from io import StringIO
from fastapi.responses import StreamingResponse
from app.core.security import hash_password
router = APIRouter(prefix="/users", tags=["users"])
async def _member_out(m: OrgMember) -> dict:
user = await User.get(m.user_id) if m.user_id else None
return {
"member_id": str(m.id),
"user_id": m.user_id,
"name": user.name if user else (m.invited_email or "Invited user"),
"email": user.email if user else m.invited_email,
"avatar_url": user.avatar_url if user else None,
"role": m.role,
"department": m.department,
"status": m.status,
"last_login": user.last_login if user else None,
"joined_at": m.joined_at or m.created_at,
}
from collections import Counter
@router.get("")
async def list_users(p: Principal = Depends(require_org)):
members = await OrgMember.find(OrgMember.org_id == str(p.org.id)).to_list()
rows = [await _member_out(m) for m in members]
role_summary = Counter()
for r in rows:
role_summary[r["role"]] += 1
plan = await Plan.get(p.org.plan_id) if p.org.plan_id else None
return {
"items": rows,
"role_summary": dict(role_summary),
"kpis": {
"total": len(rows),
"active": sum(1 for r in rows if r["status"] == "active"),
"pending": sum(1 for r in rows if r["status"] == "pending"),
"disabled": sum(1 for r in rows if r["status"] == "disabled"),
"user_limit": plan.user_limit if plan else None,
},
}
@router.get("/export")
async def export_users(
p: Principal = Depends(require_client_roles(ClientRole.admin))
):
members = await OrgMember.find(
OrgMember.org_id == str(p.org.id)
).to_list()
rows = [await _member_out(m) for m in members]
output = StringIO()
writer = csv.writer(output)
writer.writerow([
"Name",
"Email",
"Role",
"Department",
"Status",
"Last Login",
"Joined On",
])
for r in rows:
writer.writerow([
r["name"],
r["email"],
r["role"],
r["department"] or "",
r["status"],
r["last_login"] or "",
r["joined_at"] or "",
])
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={
"Content-Disposition":
"attachment; filename=users.csv"
},
)
@router.post("/invite", status_code=201)
async def invite_user(
body: dict = Body(...),
p: Principal = Depends(require_client_roles(ClientRole.admin))
):
email = body["email"].strip().lower()
if await User.find_one(User.email == email):
raise HTTPException(
status.HTTP_409_CONFLICT,
"Email already exists",
)
user = User(
name=body["name"],
email=email,
hashed_password=hash_password(body["password"]),
email_verified=True,
)
await user.insert()
member = OrgMember(
org_id=str(p.org.id),
user_id=str(user.id),
invited_email=email,
role=ClientRole(body["role"]),
department=body.get("department"),
status="active",
joined_at=now(),
)
await member.insert()
await AuditLog(
org_id=str(p.org.id),
user_id=str(p.user.id),
user_name=p.user.name,
action="user.create",
module="users",
details=f"Created user {email}",
).insert()
return await _member_out(member)
@router.get("/{member_id}/activity")
async def user_activity(member_id: str,
p: Principal = Depends(require_org)):
logs = await (
AuditLog.find(
AuditLog.org_id == str(p.org.id),
AuditLog.target_member_id == member_id,
)
.sort(-AuditLog.created_at)
.to_list()
)
return {
"items": [
{
"id": str(log.id),
"action": log.action,
"module": log.module,
"details": log.details,
"created_at": log.created_at,
}
for log in logs
]
}
@router.put("/{member_id}")
async def update_member(
member_id: str,
body: dict = Body(...),
p: Principal = Depends(require_client_roles(ClientRole.admin))
):
# Find member
m = await OrgMember.get(member_id)
if not m or m.org_id != str(p.org.id):
raise HTTPException(
status.HTTP_404_NOT_FOUND,
"Member not found"
)
# Find linked user
user = await User.get(m.user_id)
if user:
# Update name
if body.get("name"):
user.name = body["name"]
# Update email (optional)
if body.get("email"):
user.email = body["email"].strip().lower()
# Update password (optional)
if body.get("password"):
user.hashed_password = hash_password(body["password"])
await user.save()
# Update OrgMember fields
if "role" in body:
m.role = ClientRole(body["role"])
if "department" in body:
m.department = body["department"]
if (
"status" in body
and body["status"] in ("active", "disabled", "pending")
):
m.status = body["status"]
await m.save()
# Audit log
await AuditLog(
org_id=str(p.org.id),
user_id=str(p.user.id),
user_name=p.user.name,
action="user.update",
module="users",
details=f"Updated member {member_id}",
).insert()
return await _member_out(m)
@router.delete("/{member_id}", status_code=204)
async def remove_member(member_id: str,
p: Principal = Depends(require_client_roles(ClientRole.admin))):
m = await OrgMember.get(member_id)
if not m or m.org_id != str(p.org.id):
raise HTTPException(status.HTTP_404_NOT_FOUND, "Member not found")
if m.user_id == str(p.user.id):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "You cannot remove yourself")
await m.delete()
p.org.user_count = max(1, p.org.user_count - 1)
await p.org.save()