import json from typing import Any from uuid import uuid4 from ariadne import MutationType, ObjectType, QueryType, convert_kwargs_to_snake_case from graphql import GraphQLError from sqlalchemy import select from app.audit.service import write_event from app.core.jwt import create_access_token from app.core.passwords import ( WeakPasswordError, hash_password, validate_policy, ) from app.models import AuditEvent, OAuthClient, User # Создать объекты QueryType() и MutationType() из Ariadne # к ним будут привязываться резолверы через декораторы @query.field("...") и @mutation.field("...") query = QueryType() mutation = MutationType() audit_event = ObjectType("AuditEvent") # me – возвращает `info.context["user"]`; @query.field("me") def resolve_me(_, info: Any) -> Any: return info.context["user"] # читает из контекста db, # применяет поиск (ilike) и пагинацию (offset/limit), # возвращает список; @query.field("users") def resolve_users( _, info: Any, search: str | None = None, limit: int | None = None, offset: int = 0 ) -> list[User]: db = info.context["db"] stmt = select(User) if search: stmt = stmt.where(User.email.ilike(f"%{search}%")) stmt = stmt.order_by(User.id).offset(offset or 0) if limit: stmt = stmt.limit(limit) return list(db.scalars(stmt).all()) # clients – возвращает всех клиентов; @query.field("clients") def resolve_clients(_, info: Any) -> list[OAuthClient]: db = info.context["db"] return list(db.scalars(select(OAuthClient).order_by(OAuthClient.id)).all()) # auditEvents – фильтрует по eventType, # сортирует по createdAt desc, применяет пагинацию. @query.field("auditEvents") @convert_kwargs_to_snake_case def resolve_audit_events( _, info: Any, event_type: str | None = None, limit: int | None = None, offset: int = 0, ) -> list[AuditEvent]: db = info.context["db"] stmt = select(AuditEvent) if event_type: stmt = stmt.where(AuditEvent.event_type == event_type) stmt = stmt.order_by(AuditEvent.created_at.desc()).offset(offset or 0) if limit: stmt = stmt.limit(limit) return list(db.scalars(stmt).all()) # повторяет логику регистрации из REST-эндпоинта: # проверка email, # validate_policy, # hash_password, # запись аудита, # возврат пары токенов; @mutation.field("registerUser") @convert_kwargs_to_snake_case def resolve_register_user(_, info: Any, email: str, password: str) -> dict[str, str]: db = info.context["db"] if db.scalar(select(User).where(User.email == email)) is not None: raise GraphQLError("Email already registered") try: validate_policy(password, user_inputs=[email]) except WeakPasswordError as exc: raise GraphQLError(str(exc)) from exc user = User(email=email, hashed_password=hash_password(password)) db.add(user) db.commit() db.refresh(user) write_event(db, "user.created", actor_id=user.id) return {"access_token": create_access_token(str(user.id)), "token_type": "bearer"} # генерирует случайный client_id (например, через uuid4), # сохраняет, пишет событие аудита; @mutation.field("createClient") @convert_kwargs_to_snake_case def resolve_create_client( _, info: Any, name: str, is_confidential: bool = True ) -> OAuthClient: db = info.context["db"] client = OAuthClient( client_id=uuid4().hex, name=name, is_confidential=is_confidential ) db.add(client) db.commit() db.refresh(client) write_event(db, "client.created", client_id=client.client_id) return client # ищет по id, ставит is_active = False, пишет аудит. @mutation.field("disableUser") @convert_kwargs_to_snake_case def resolve_disable_user(_, info: Any, user_id: int) -> User: db = info.context["db"] user = db.get(User, user_id) if user is None: raise GraphQLError("User not found") user.is_active = False db.commit() db.refresh(user) actor = info.context.get("user") write_event(db, "user.disabled", actor_id=actor.id if actor else None) return user @audit_event.field("metadata") def resolve_metadata(obj: AuditEvent, info: Any) -> str | None: if obj.metadata_json is None: return None return json.dumps(obj.metadata_json, ensure_ascii=False)