from datetime import timedelta from ipaddress import ip_address from django.contrib.auth import ( authenticate, get_user_model, login, logout, update_session_auth_hash, ) from django.contrib.auth.models import Group, Permission from django.contrib.auth.password_validation import validate_password from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db.models import Q from django.middleware.csrf import get_token from django.utils import timezone from ninja import Router from ninja.errors import HttpError from apps.accounts.models import MenuItem, UserGroup from apps.accounts.permissions import PERMISSIONS from apps.accounts.schemas import ( BasicUserOut, CurrentUserOut, IdsIn, LoginIn, MenuOut, MenuUpdateIn, PasswordChangeIn, PasswordResetIn, PermissionCodesIn, PermissionOut, ProfileStatsOut, ProfileUpdateIn, RoleCreateIn, RoleDetailOut, RoleUpdateIn, UserCreateIn, UserGroupCreateIn, UserGroupOut, UserGroupUpdateIn, UserUpdateIn, ) from apps.audit.models import OperationAuditLog from apps.tickets.models import Ticket, TicketEvent, TicketStatus router = Router(tags=["accounts"]) def permission_codes_for_user(user) -> list[str]: if user.is_superuser: return sorted(permission.codename for permission in PERMISSIONS) codes = set() for permission in user.get_all_permissions(): if permission.startswith("system."): codes.add(permission.removeprefix("system.")) return sorted(codes) def permission_objects_for_codes(codes: list[str]): content_type = ContentType.objects.get(app_label="system", model="platformpermission") return list(Permission.objects.filter(content_type=content_type, codename__in=codes)) def serialize_menu(menu: MenuItem, children: list[dict]) -> dict: return { "id": menu.id, "code": menu.code, "title": menu.title, "path": menu.path, "icon": menu.icon, "permission_code": menu.permission_code, "is_visible": menu.is_visible, "is_enabled": menu.is_enabled, "children": children, } def menu_tree_for_permissions(permission_codes: list[str]) -> list[dict]: allowed = set(permission_codes) menus = list( MenuItem.objects.filter(is_visible=True, is_enabled=True) .select_related("parent") .order_by("sort_order", "id") ) children_by_parent: dict[int | None, list[MenuItem]] = {} for menu in menus: if menu.permission_code and menu.permission_code not in allowed: continue children_by_parent.setdefault(menu.parent_id, []).append(menu) def build(parent_id: int | None) -> list[dict]: result = [] for menu in children_by_parent.get(parent_id, []): result.append(serialize_menu(menu, build(menu.id))) return result return build(None) def serialize_current_user(user) -> dict: permission_codes = permission_codes_for_user(user) return { "id": user.id, "username": user.username, "display_name": user.get_full_name() or user.username, "email": user.email, "last_login": user.last_login, "date_joined": user.date_joined, "is_superuser": user.is_superuser, "roles": [{"id": group.id, "name": group.name} for group in user.groups.all()], "user_groups": [ { "id": group.id, "name": group.name, "code": group.code, "description": group.description, "is_active": group.is_active, "created_at": group.created_at, "updated_at": group.updated_at, } for group in user.business_groups.all() ], "permissions": permission_codes, "menus": menu_tree_for_permissions(permission_codes), } def serialize_basic_user(user) -> dict: return { "id": user.id, "username": user.username, "display_name": user.get_full_name() or user.username, "email": user.email, "is_active": user.is_active, "is_staff": user.is_staff, "is_superuser": user.is_superuser, "roles": [{"id": group.id, "name": group.name} for group in user.groups.all()], "user_groups": [ { "id": group.id, "name": group.name, "code": group.code, "description": group.description, "is_active": group.is_active, "created_at": group.created_at, "updated_at": group.updated_at, } for group in user.business_groups.all() ], } def serialize_role(group: Group) -> dict: codes = sorted(group.permissions.values_list("codename", flat=True)) return {"id": group.id, "name": group.name, "permission_codes": codes} def serialize_group(group) -> dict: return { "id": group.id, "name": group.name, "code": group.code, "description": group.description, "is_active": group.is_active, "created_at": group.created_at, "updated_at": group.updated_at, } def serialize_menu_flat(menu: MenuItem) -> dict: return serialize_menu(menu, []) def require_login(request): if not request.user.is_authenticated: raise HttpError(401, "Authentication required") return request.user def require_permission(request, codename: str): user = require_login(request) if user.is_superuser or user.has_perm(f"system.{codename}"): return user raise HttpError(403, "Permission denied") def validate_user_password(password: str, user) -> None: try: validate_password(password, user=user) except ValidationError as error: raise HttpError(400, ";".join(error.messages)) from error def account_audit(request, action: str, target, snapshot: dict) -> None: forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "") raw_ip_address = forwarded_for.split(",")[0].strip() or request.META.get("REMOTE_ADDR", "") try: audit_ip_address = str(ip_address(raw_ip_address)) except ValueError: audit_ip_address = None OperationAuditLog.objects.create( user=request.user if getattr(request.user, "is_authenticated", False) else None, module="accounts", action=action, target_type="user", target_id=str(target.id), request_snapshot=snapshot, result="success", ip_address=audit_ip_address, user_agent=request.META.get("HTTP_USER_AGENT", ""), ) def protect_admin_target(actor, target) -> None: if target.is_superuser and not actor.is_superuser: raise HttpError(403, "只有超级管理员可以管理其他超级管理员账号") @router.get("/auth/csrf") def csrf(request): return {"csrfToken": get_token(request)} @router.post("/auth/login", response=CurrentUserOut) def login_user(request, payload: LoginIn): user = authenticate(request, username=payload.username, password=payload.password) if user is None or not user.is_active: raise HttpError(401, "Invalid username or password") login(request, user) return serialize_current_user(user) @router.post("/auth/logout") def logout_user(request): logout(request) return {"status": "ok"} @router.get("/auth/me", response=CurrentUserOut) def current_user(request): user = require_login(request) return serialize_current_user(user) @router.patch("/auth/me", response=CurrentUserOut) def update_current_user(request, payload: ProfileUpdateIn): user = require_login(request) values = payload.dict(exclude_unset=True) update_fields = [] if "display_name" in values: user.first_name = values["display_name"] or "" update_fields.append("first_name") if "email" in values: user.email = values["email"] or "" update_fields.append("email") if update_fields: user.save(update_fields=update_fields) account_audit(request, "update_profile", user, {"fields": sorted(update_fields)}) return serialize_current_user(user) @router.post("/auth/change-password") def change_current_user_password(request, payload: PasswordChangeIn): user = require_login(request) if not user.check_password(payload.current_password): raise HttpError(400, "当前密码不正确") validate_user_password(payload.new_password, user) user.set_password(payload.new_password) user.save(update_fields=["password"]) update_session_auth_hash(request, user) account_audit(request, "change_password", user, {"fields": ["password"]}) return {"status": "ok"} @router.get("/auth/me/stats", response=ProfileStatsOut) def current_user_stats(request): user = require_login(request) completed_statuses = [TicketStatus.RESOLVED, TicketStatus.CLOSED] assigned = Ticket.objects.filter(assignee=user) assigned_total = assigned.count() completed_total = assigned.filter(status__in=completed_statuses).count() completed_cutoff = timezone.now() - timedelta(days=30) completed_30d = assigned.filter(status__in=completed_statuses).filter( Q(resolved_at__gte=completed_cutoff) | Q(closed_at__gte=completed_cutoff) ).count() activity = ( TicketEvent.objects.select_related("ticket") .filter(operator=user) .order_by("-created_at")[:8] ) return { "created_total": Ticket.objects.filter(created_by=user).count(), "active_assigned": assigned.exclude(status__in=completed_statuses).count(), "completed_30d": completed_30d, "completion_rate": round(completed_total * 100 / assigned_total, 1) if assigned_total else 0, "recent_activity": [ { "id": event.id, "ticket_id": event.ticket_id, "ticket_no": event.ticket.ticket_no, "ticket_title": event.ticket.title, "event_type": event.event_type, "created_at": event.created_at, } for event in activity ], } @router.get("/users", response=list[BasicUserOut]) def list_users(request): require_permission(request, "system.user.view") return [serialize_basic_user(user) for user in get_user_model().objects.all().order_by("id")] @router.post("/users", response=BasicUserOut) def create_user(request, payload: UserCreateIn): require_permission(request, "system.user.manage") candidate = get_user_model()( username=payload.username, email=payload.email, first_name=payload.display_name, ) validate_user_password(payload.password, candidate) user = get_user_model().objects.create_user( username=payload.username, email=payload.email, password=payload.password, is_active=payload.is_active, ) if payload.display_name: user.first_name = payload.display_name user.save(update_fields=["first_name"]) account_audit( request, "create_user", user, {"fields": ["username", "display_name", "email", "is_active"]}, ) return serialize_basic_user(user) @router.get("/users/{user_id}", response=BasicUserOut) def get_user(request, user_id: int): require_permission(request, "system.user.view") return serialize_basic_user(get_user_model().objects.get(id=user_id)) @router.patch("/users/{user_id}", response=BasicUserOut) def update_user(request, user_id: int, payload: UserUpdateIn): actor = require_permission(request, "system.user.manage") user = get_user_model().objects.get(id=user_id) protect_admin_target(actor, user) if actor.id == user.id and payload.is_active is False: raise HttpError(400, "不能停用当前登录账号") update_fields = [] if payload.display_name is not None: user.first_name = payload.display_name update_fields.append("first_name") if payload.email is not None: user.email = payload.email update_fields.append("email") if payload.is_active is not None: user.is_active = payload.is_active update_fields.append("is_active") if update_fields: user.save(update_fields=update_fields) account_audit(request, "update_user", user, {"fields": sorted(update_fields)}) return serialize_basic_user(user) @router.post("/users/{user_id}/reset-password", response=BasicUserOut) def reset_user_password(request, user_id: int, payload: PasswordResetIn): actor = require_permission(request, "system.user.manage") user = get_user_model().objects.get(id=user_id) protect_admin_target(actor, user) if actor.id == user.id: raise HttpError(400, "请前往个人中心修改当前账号密码") validate_user_password(payload.password, user) user.set_password(payload.password) user.save(update_fields=["password"]) account_audit(request, "reset_password", user, {"fields": ["password"]}) return serialize_basic_user(user) @router.post("/users/{user_id}/roles", response=BasicUserOut) def set_user_roles(request, user_id: int, payload: IdsIn): actor = require_permission(request, "system.user.manage") user = get_user_model().objects.get(id=user_id) protect_admin_target(actor, user) user.groups.set(Group.objects.filter(id__in=payload.role_ids)) account_audit(request, "set_roles", user, {"role_count": len(payload.role_ids)}) return serialize_basic_user(user) @router.post("/users/{user_id}/user-groups", response=BasicUserOut) def set_user_groups(request, user_id: int, payload: IdsIn): actor = require_permission(request, "system.user.manage") user = get_user_model().objects.get(id=user_id) protect_admin_target(actor, user) user.business_groups.set(UserGroup.objects.filter(id__in=payload.user_ids)) account_audit(request, "set_user_groups", user, {"user_group_count": len(payload.user_ids)}) return serialize_basic_user(user) @router.get("/user-groups", response=list[UserGroupOut]) def list_user_groups(request): require_permission(request, "system.user_group.view") return [serialize_group(group) for group in UserGroup.objects.all()] @router.post("/user-groups", response=UserGroupOut) def create_user_group(request, payload: UserGroupCreateIn): require_permission(request, "system.user_group.manage") group = UserGroup.objects.create(**payload.dict()) return serialize_group(group) @router.patch("/user-groups/{group_id}", response=UserGroupOut) def update_user_group(request, group_id: int, payload: UserGroupUpdateIn): require_permission(request, "system.user_group.manage") group = UserGroup.objects.get(id=group_id) for field, value in payload.dict(exclude_unset=True).items(): setattr(group, field, value) group.save() return serialize_group(group) @router.post("/user-groups/{group_id}/members", response=UserGroupOut) def set_user_group_members(request, group_id: int, payload: IdsIn): require_permission(request, "system.user_group.manage") group = UserGroup.objects.get(id=group_id) group.users.set(get_user_model().objects.filter(id__in=payload.user_ids)) return serialize_group(group) @router.get("/roles", response=list[RoleDetailOut]) def list_roles(request): require_permission(request, "system.role.view") return [serialize_role(group) for group in Group.objects.all().order_by("id")] @router.post("/roles", response=RoleDetailOut) def create_role(request, payload: RoleCreateIn): require_permission(request, "system.role.manage") group = Group.objects.create(name=payload.name) group.permissions.set(permission_objects_for_codes(payload.permission_codes)) return serialize_role(group) @router.patch("/roles/{role_id}", response=RoleDetailOut) def update_role(request, role_id: int, payload: RoleUpdateIn): require_permission(request, "system.role.manage") group = Group.objects.get(id=role_id) if payload.name is not None: group.name = payload.name group.save(update_fields=["name"]) return serialize_role(group) @router.post("/roles/{role_id}/permissions", response=RoleDetailOut) def set_role_permissions(request, role_id: int, payload: PermissionCodesIn): require_permission(request, "system.role.manage") group = Group.objects.get(id=role_id) group.permissions.set(permission_objects_for_codes(payload.permission_codes)) return serialize_role(group) @router.get("/permissions", response=list[PermissionOut]) def list_permissions(request): require_permission(request, "system.role.view") return [ {"codename": permission.codename, "name": permission.name, "module": permission.module} for permission in PERMISSIONS ] @router.get("/menus", response=list[MenuOut]) def list_menus(request): require_permission(request, "system.menu.view") return [serialize_menu_flat(menu) for menu in MenuItem.objects.all()] @router.patch("/menus/{menu_id}", response=MenuOut) def update_menu(request, menu_id: int, payload: MenuUpdateIn): require_permission(request, "system.menu.manage") menu = MenuItem.objects.get(id=menu_id) for field, value in payload.dict(exclude_unset=True).items(): setattr(menu, field, value) menu.save() return serialize_menu_flat(menu)