From df6b34206d17b032aedd593165acc4da4b17b30a Mon Sep 17 00:00:00 2001 From: Agzamus Date: Mon, 12 Oct 2026 16:58:30 +0300 Subject: [PATCH] =?UTF-8?q?feat:=209.2.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B5=D0=B7=D0=BE=D0=BB?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/graphql/resolvers.py | 130 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 app/graphql/resolvers.py diff --git a/app/graphql/resolvers.py b/app/graphql/resolvers.py new file mode 100644 index 0000000..50f3e6c --- /dev/null +++ b/app/graphql/resolvers.py @@ -0,0 +1,130 @@ +from typing import Any +from uuid import uuid4 + +from ariadne import MutationType, 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() + + +# 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