diff --git a/logs/api.log b/logs/api.log
index 1e121d7..9ef43d2 100644
--- a/logs/api.log
+++ b/logs/api.log
@@ -191,3 +191,94 @@
2025-08-08 21:56:21 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/v1/profiles/me | headers={Authorization: Bearer eyJhbGciOiJI...} | body={}
2025-08-08 21:56:21 | DEBUG | api_e2e | ← 403 in 2 ms | body={"detail":"Not authenticated"}
2025-08-08 21:56:21 | ERROR | api_e2e | profiles/me unexpected status 403, expected [200, 404]; body={"detail":"Not authenticated"}
+2025-08-08 22:21:56 | INFO | api_e2e | === API E2E START ===
+2025-08-08 22:21:56 | INFO | api_e2e | BASE_URL=http://localhost:8080 clients=2 domain=agency.dev
+2025-08-08 22:21:56 | INFO | api_e2e | Waiting gateway/auth health: http://localhost:8080/auth/health
+2025-08-08 22:21:56 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:21:59 | DEBUG | api_e2e | ← 502 in 3056 ms | body=
+
502 Bad Gateway
+
+502 Bad Gateway
+
nginx/1.29.0
+
+
+
+2025-08-08 22:21:59 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body=
+502 Bad Gateway
+
+502 Bad Gateway
+
nginx/1.29.0
+
+
+
+2025-08-08 22:22:00 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:22:04 | DEBUG | api_e2e | ← 502 in 3095 ms | body=
+502 Bad Gateway
+
+502 Bad Gateway
+
nginx/1.29.0
+
+
+
+2025-08-08 22:22:04 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body=
+502 Bad Gateway
+
+502 Bad Gateway
+
nginx/1.29.0
+
+
+
+2025-08-08 22:22:05 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | INFO | api_e2e | === API E2E START ===
+2025-08-08 22:31:43 | INFO | api_e2e | BASE_URL=http://localhost:8080 clients=2 domain=agency.dev
+2025-08-08 22:31:43 | INFO | api_e2e | Waiting gateway/auth health: http://localhost:8080/auth/health
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 3 ms | body={"status":"ok","service":"auth"}
+2025-08-08 22:31:43 | INFO | api_e2e | gateway/auth is healthy
+2025-08-08 22:31:43 | INFO | api_e2e | Waiting profiles health: http://localhost:8080/profiles/health
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 3 ms | body={"status":"ok","service":"profiles"}
+2025-08-08 22:31:43 | INFO | api_e2e | profiles is healthy
+2025-08-08 22:31:43 | INFO | api_e2e | Waiting match health: http://localhost:8080/match/health
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/match/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"match"}
+2025-08-08 22:31:43 | INFO | api_e2e | match is healthy
+2025-08-08 22:31:43 | INFO | api_e2e | Waiting chat health: http://localhost:8080/chat/health
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/chat/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"chat"}
+2025-08-08 22:31:43 | INFO | api_e2e | chat is healthy
+2025-08-08 22:31:43 | INFO | api_e2e | Waiting payments health: http://localhost:8080/payments/health
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/payments/health | headers={Authorization: Bearer } | body={}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"payments"}
+2025-08-08 22:31:43 | INFO | api_e2e | payments is healthy
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:43 | DEBUG | api_e2e | ← 401 in 4 ms | body={"detail":"Invalid credentials"}
+2025-08-08 22:31:43 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
+2025-08-08 22:31:43 | INFO | api_e2e | Login failed for admin+1754659903.xaji0y@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
+2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***', 'full_name': 'Kimberly Banks', 'role': 'ADMIN'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 201 in 227 ms | body={"id":"caf24a32-42bd-491e-9ff3-31e2eb0211ed","email":"admin+1754659903.xaji0y@agency.dev","full_name":"Kimberly Banks","role":"ADMIN","is_active":true}
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjYWYyNGEzMi00MmJkLTQ5MWUtOWZmMy0zMWUyZWIwMjExZWQiLCJlbWFpbCI6ImFkbWluKzE3NTQ2NTk5MDMueGFqaTB5QGFnZW5jeS5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0NjYwODA0fQ.TBg4_Xu2js-yD6aIjx-BAt3n8MJtM4K5Ck1Yc-ZG8sg","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjYWYyNGEzMi00MmJkLTQ5MWUtOWZmMy0zMWUyZWIwMjExZWQiLCJlbWFpbCI6ImFkbWluKzE3NTQ2NTk5MDMueGFqaTB5QGFnZW5jeS5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzI1MTkwNH0.W0AsJT8t9QQTZhKKTiKk0Q-pZtpnCxp_Kw75h1f7yJA","token_type":"bearer"}
+2025-08-08 22:31:44 | INFO | api_e2e | Registered+Login OK: admin+1754659903.xaji0y@agency.dev -> caf24a32-42bd-491e-9ff3-31e2eb0211ed
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 401 in 4 ms | body={"detail":"Invalid credentials"}
+2025-08-08 22:31:44 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
+2025-08-08 22:31:44 | INFO | api_e2e | Login failed for user1+1754659904.6dpbhs@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***', 'full_name': 'Jose Meyer', 'role': 'CLIENT'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 201 in 217 ms | body={"id":"f7f05b43-00f6-436d-ab66-6e81c5ccbc33","email":"user1+1754659904.6dpbhs@agency.dev","full_name":"Jose Meyer","role":"CLIENT","is_active":true}
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmN2YwNWI0My0wMGY2LTQzNmQtYWI2Ni02ZTgxYzVjY2JjMzMiLCJlbWFpbCI6InVzZXIxKzE3NTQ2NTk5MDQuNmRwYmhzQGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDY2MDgwNH0.CJA6gG8WJdEPMSQSKLAPBIRY3JMA34TGjveNICF_d-I","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmN2YwNWI0My0wMGY2LTQzNmQtYWI2Ni02ZTgxYzVjY2JjMzMiLCJlbWFpbCI6InVzZXIxKzE3NTQ2NTk5MDQuNmRwYmhzQGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTcyNTE5MDR9.aYdrjuY5smBsgrF_6EtP83P2d_700yVIdlWDbPXM5DU","token_type":"bearer"}
+2025-08-08 22:31:44 | INFO | api_e2e | Registered+Login OK: user1+1754659904.6dpbhs@agency.dev -> f7f05b43-00f6-436d-ab66-6e81c5ccbc33
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:44 | DEBUG | api_e2e | ← 401 in 3 ms | body={"detail":"Invalid credentials"}
+2025-08-08 22:31:44 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
+2025-08-08 22:31:44 | INFO | api_e2e | Login failed for user2+1754659904.ahxthv@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
+2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***', 'full_name': 'Teresa Mckenzie', 'role': 'CLIENT'}
+2025-08-08 22:31:45 | DEBUG | api_e2e | ← 201 in 225 ms | body={"id":"540546d6-563c-452c-b4bc-b0a0ce977b50","email":"user2+1754659904.ahxthv@agency.dev","full_name":"Teresa Mckenzie","role":"CLIENT","is_active":true}
+2025-08-08 22:31:45 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***'}
+2025-08-08 22:31:45 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NDA1NDZkNi01NjNjLTQ1MmMtYjRiYy1iMGEwY2U5NzdiNTAiLCJlbWFpbCI6InVzZXIyKzE3NTQ2NTk5MDQuYWh4dGh2QGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDY2MDgwNX0.wYH9Tf_Q1oz0u_hxzyLYrOmAur_RxJyySxkJgxXOIqY","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NDA1NDZkNi01NjNjLTQ1MmMtYjRiYy1iMGEwY2U5NzdiNTAiLCJlbWFpbCI6InVzZXIyKzE3NTQ2NTk5MDQuYWh4dGh2QGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTcyNTE5MDV9.TsULoLRT4s40aOD0IUE6uamDyKAvSHiw9AW9EfacVeo","token_type":"bearer"}
+2025-08-08 22:31:45 | INFO | api_e2e | Registered+Login OK: user2+1754659904.ahxthv@agency.dev -> 540546d6-563c-452c-b4bc-b0a0ce977b50
+2025-08-08 22:31:45 | INFO | api_e2e | [1/3] Ensure profile for admin+1754659903.xaji0y@agency.dev (role=ADMIN)
+2025-08-08 22:31:45 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/v1/profiles/me | headers={Authorization: Bearer eyJhbGciOiJI...} | body={}
+2025-08-08 22:31:45 | DEBUG | api_e2e | ← 403 in 2 ms | body={"detail":"Not authenticated"}
+2025-08-08 22:31:45 | ERROR | api_e2e | profiles/me unexpected status 403, expected [200, 404]; body={"detail":"Not authenticated"}
diff --git a/scripts/fix_auth_response_model.sh b/scripts/fix_auth_response_model.sh
new file mode 100755
index 0000000..37fcb95
--- /dev/null
+++ b/scripts/fix_auth_response_model.sh
@@ -0,0 +1,32 @@
+set -euo pipefail
+FILE="services/auth/src/app/schemas/user.py"
+[ -f "$FILE" ] || { echo "Not found: $FILE"; exit 1; }
+
+python3 - "$FILE" <<'PY'
+from pathlib import Path
+import re, sys
+p = Path(sys.argv[1]); s = p.read_text()
+
+if "from uuid import UUID" not in s:
+ s = "from uuid import UUID\n" + s
+
+s = re.sub(r'(\bid\s*:\s*)str', r'\1UUID', s)
+s = re.sub(r'(\buser_id\s*:\s*)str', r'\1UUID', s)
+
+# Включаем from_attributes (pydantic v2) или orm_mode (v1) для моделей ответа
+if "model_config" not in s and "orm_mode" not in s:
+ s = re.sub(
+ r"class\s+\w+Out\s*\((?:.|\n)*?\):",
+ lambda m: m.group(0) +
+ "\n try:\n from pydantic import ConfigDict\n model_config = ConfigDict(from_attributes=True)\n"
+ " except Exception:\n class Config:\n orm_mode = True\n",
+ s
+ )
+
+p.write_text(s)
+print("[auth] Patched:", p)
+PY
+
+echo "[auth] Rebuild & restart…"
+docker compose build auth
+docker compose restart auth
diff --git a/scripts/fix_chat_uuid_schemas.sh b/scripts/fix_chat_uuid_schemas.sh
new file mode 100755
index 0000000..6fc188b
--- /dev/null
+++ b/scripts/fix_chat_uuid_schemas.sh
@@ -0,0 +1,117 @@
+set -euo pipefail
+
+svc_dir="services/chat/src/app/schemas"
+test -d "$svc_dir" || { echo "Not found: $svc_dir"; exit 1; }
+
+python3 - <<'PY'
+import re
+from pathlib import Path
+
+root = Path("services/chat/src/app/schemas")
+
+def ensure_future_and_uuid(text: str) -> str:
+ lines = text.splitlines(keepends=True)
+ changed = False
+
+ # Удалим дубли future-импорта
+ fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
+ if fut_pat.search(text):
+ lines = [ln for ln in lines if not fut_pat.match(ln)]
+ changed = True
+
+ # Вставим future после докстринга (если он есть)
+ i = 0
+ while i < len(lines) and (lines[i].strip()=="" or lines[i].lstrip().startswith("#!") or re.match(r'^\s*#.*coding[:=]', lines[i])):
+ i += 1
+ insert_at = i
+ if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]): # докстринг
+ q = '"""' if '"""' in lines[i] else "'''"
+ j = i
+ while j < len(lines):
+ if q in lines[j] and j != i:
+ j += 1
+ break
+ j += 1
+ insert_at = min(j, len(lines))
+
+ if not any("from __future__ import annotations" in ln for ln in lines):
+ lines.insert(insert_at, "from __future__ import annotations\n")
+ changed = True
+
+ txt = "".join(lines)
+ if "from uuid import UUID" not in txt:
+ # вставим сразу после future
+ # найдём позицию future
+ pos = next((k for k,ln in enumerate(lines) if "from __future__ import annotations" in ln), 0)
+ lines.insert(pos+1, "from uuid import UUID\n")
+ changed = True
+
+ return "".join(lines), changed
+
+def set_config(text: str) -> str:
+ # Pydantic v2: добавим ConfigDict и model_config, если их нет
+ if "ConfigDict" not in text:
+ text = re.sub(r'from pydantic import ([^\n]+)',
+ lambda m: m.group(0).replace(m.group(1), (m.group(1) + ", ConfigDict").replace(", ConfigDict,"," , ConfigDict,")),
+ text, count=1) if "from pydantic import" in text else ("from pydantic import BaseModel, ConfigDict\n" + text)
+
+ # В каждую модель, наследующую BaseModel, которая заканчивается на Read/Out, добавим from_attributes
+ def inject_cfg(block: str) -> str:
+ # если уже есть model_config/orm_mode — не трогаем
+ if "model_config" in block or "class Config" in block:
+ return block
+ # аккуратно добавим model_config сверху класса
+ header, body = block.split(":", 1)
+ return header + ":\n model_config = ConfigDict(from_attributes=True)\n" + body
+
+ # Разобьём по классам
+ parts = re.split(r'(\nclass\s+[A-Za-z0-9_]+\s*\([^\)]*BaseModel[^\)]*\)\s*:)', text)
+ if len(parts) > 1:
+ out = [parts[0]]
+ for i in range(1, len(parts), 2):
+ head = parts[i]
+ body = parts[i+1]
+ m = re.search(r'class\s+([A-Za-z0-9_]+)\s*\(', head)
+ name = m.group(1) if m else ""
+ if name.endswith(("Read","Out")):
+ out.append(inject_cfg(head + body))
+ else:
+ out.append(head + body)
+ text = "".join(out)
+ return text
+
+def fix_uuid_fields(text: str) -> str:
+ # Преобразуем только в классах Read/Out
+ def repl_in_class(cls_text: str) -> str:
+ # заменяем аннотации полей
+ repl = {
+ r'(^\s*)(id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(pair_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(room_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(sender_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(user_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ }
+ for pat, sub in repl.items():
+ cls_text = re.sub(pat, sub, cls_text, flags=re.M)
+ return cls_text
+
+ patt = re.compile(r'(\nclass\s+[A-Za-z0-9_]+(Read|Out)\s*\([^\)]*BaseModel[^\)]*\)\s*:\n(?:\s+.*\n)+)', re.M)
+ return patt.sub(lambda m: repl_in_class(m.group(1)), text)
+
+changed_any = False
+for p in root.glob("**/*.py"):
+ txt = p.read_text()
+ new, c1 = ensure_future_and_uuid(txt)
+ new = set_config(new)
+ new2 = fix_uuid_fields(new)
+ if new2 != txt:
+ p.write_text(new2)
+ print("fixed:", p)
+ changed_any = True
+
+print("DONE" if changed_any else "NO-CHANGES")
+PY
+
+echo "[docker] rebuild & restart chat…"
+docker compose build --no-cache chat >/dev/null
+docker compose restart chat
diff --git a/scripts/fix_future_imports.sh b/scripts/fix_future_imports.sh
new file mode 100755
index 0000000..a3131f5
--- /dev/null
+++ b/scripts/fix_future_imports.sh
@@ -0,0 +1,93 @@
+set -euo pipefail
+
+python3 - <<'PY'
+import re
+from pathlib import Path
+
+def fix_file(p: Path) -> bool:
+ s = p.read_text()
+ lines = s.splitlines(keepends=True)
+ changed = False
+
+ # собрать все строки future-импортов и убрать их
+ fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
+ if fut_pat.search(s):
+ new_lines = []
+ for ln in lines:
+ if not fut_pat.match(ln):
+ new_lines.append(ln)
+ lines = new_lines
+ changed = True
+
+ # определить позицию вставки future (сразу после верхнего докстринга, если он есть)
+ i = 0
+ # пропускаем пустые строки и комментарии/кодировки/шейбанг
+ while i < len(lines) and (
+ lines[i].strip() == "" or
+ lines[i].lstrip().startswith("#!") or
+ re.match(r'^\s*#.*coding[:=]', lines[i])
+ ):
+ i += 1
+
+ insert_at = i
+ # если на вершине модульный докстринг ("""...""" или '''...''')
+ if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]):
+ quote = '"""' if '"""' in lines[i] else "'''"
+ j = i
+ # ищем закрывающую кавычку
+ while j < len(lines):
+ if quote in lines[j] and j != i:
+ j += 1
+ break
+ j += 1
+ insert_at = min(j, len(lines))
+
+ # вставляем future-импорт
+ future_line = "from __future__ import annotations\n"
+ if not any(l.strip().startswith("from __future__ import annotations") for l in lines):
+ lines.insert(insert_at, future_line)
+ changed = True
+
+ # обеспечить корректное положение "from uuid import UUID":
+ txt = "".join(lines)
+ uuid_pat = re.compile(r'^\s*from\s+uuid\s+import\s+UUID\s*(#.*)?$\n?', re.M)
+ has_uuid = uuid_pat.search(txt) is not None
+ if has_uuid:
+ # удалим все вхождения, потом вставим одно — сразу после future
+ new_lines = []
+ removed = False
+ for ln in lines:
+ if uuid_pat.match(ln):
+ removed = True
+ continue
+ new_lines.append(ln)
+ lines = new_lines
+ if removed:
+ lines.insert(insert_at + 1, "from uuid import UUID\n")
+ changed = True
+ else:
+ # импорт отсутствует — добавлять имеет смысл только если в файле встречается "UUID" как тип
+ if re.search(r'\bUUID\b', "".join(lines)):
+ lines.insert(insert_at + 1, "from uuid import UUID\n")
+ changed = True
+
+ if changed:
+ p.write_text("".join(lines))
+ return changed
+
+changed_any = False
+for sub in ("services/auth/src/app/schemas", "services/match/src/app/schemas", "services/profiles/src/app/schemas"):
+ d = Path(sub)
+ if not d.exists():
+ continue
+ for p in d.rglob("*.py"):
+ if fix_file(p):
+ print("fixed:", p)
+ changed_any = True
+
+print("DONE" if changed_any else "NO-CHANGES")
+PY
+
+echo "[docker] rebuild & restart auth/match/profiles…"
+docker compose build auth match profiles >/dev/null
+docker compose restart auth match profiles
diff --git a/scripts/fix_match_response_models.sh b/scripts/fix_match_response_models.sh
new file mode 100755
index 0000000..7bb6904
--- /dev/null
+++ b/scripts/fix_match_response_models.sh
@@ -0,0 +1,41 @@
+set -euo pipefail
+DIR="services/match/src/app/schemas"
+[ -d "$DIR" ] || { echo "Not found: $DIR"; exit 1; }
+
+python3 - "$DIR" <<'PY'
+from pathlib import Path
+import re, sys
+d = Path(sys.argv[1])
+for p in d.glob("*.py"):
+ s = p.read_text(); orig = s
+ if "class " not in s:
+ continue
+
+ if "from uuid import UUID" not in s:
+ s = "from uuid import UUID\n" + s
+
+ # самые частые поля
+ s = re.sub(r'(\bid\s*:\s*)str', r'\1UUID', s)
+ s = re.sub(r'(\buser_id_a\s*:\s*)str', r'\1UUID', s)
+ s = re.sub(r'(\buser_id_b\s*:\s*)str', r'\1UUID', s)
+ s = re.sub(r'(\buser_id\s*:\s*)str', r'\1UUID', s)
+
+ # включаем from_attributes / orm_mode для Out/Response моделей
+ def patch_block(m):
+ block = m.group(0)
+ if "model_config" in block or "orm_mode" in block:
+ return block
+ return (block +
+ "\n try:\n from pydantic import ConfigDict\n model_config = ConfigDict(from_attributes=True)\n"
+ " except Exception:\n class Config:\n orm_mode = True\n")
+
+ s = re.sub(r"class\s+\w+(Out|Response)\s*\([^\)]*\)\s*:(?:\n\s+.+)+", patch_block, s)
+
+ if s != orig:
+ p.write_text(s)
+ print("[match] Patched:", p)
+PY
+
+echo "[match] Rebuild & restart…"
+docker compose build match
+docker compose restart match
diff --git a/scripts/patch.sh b/scripts/patch.sh
index e5404b1..31f69ce 100755
--- a/scripts/patch.sh
+++ b/scripts/patch.sh
@@ -1,31 +1,120 @@
-# scripts/patch_gateway_auth_header.sh
-cat > scripts/patch_gateway_auth_header.sh <<'BASH'
-#!/usr/bin/env bash
+cat > scripts/fix_chat_uuid_schemas.sh <<'SH'
set -euo pipefail
-CFG="infra/gateway/nginx.conf"
-[ -f "$CFG" ] || { echo "Not found: $CFG"; exit 1; }
+svc_dir="services/chat/src/app/schemas"
+test -d "$svc_dir" || { echo "Not found: $svc_dir"; exit 1; }
-# Грубая, но надёжная вставка proxy_set_header Authorization во все блоки location к сервисам
-awk '
- /location[[:space:]]+\/(auth|profiles|match|chat|payments)\//,/\}/ {
- print
- if ($0 ~ /proxy_pass/ && !seen_auth) {
- print " proxy_set_header Authorization $http_authorization;"
- print " proxy_set_header X-Forwarded-Proto $scheme;"
- print " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
- print " proxy_set_header Host $host;"
- seen_auth=1
- }
- next
- }
- { print }
- /\}/ { seen_auth=0 }
-' "$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
+python3 - <<'PY'
+import re
+from pathlib import Path
+
+root = Path("services/chat/src/app/schemas")
+
+def ensure_future_and_uuid(text: str) -> str:
+ lines = text.splitlines(keepends=True)
+ changed = False
+
+ # Удалим дубли future-импорта
+ fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
+ if fut_pat.search(text):
+ lines = [ln for ln in lines if not fut_pat.match(ln)]
+ changed = True
+
+ # Вставим future после докстринга (если он есть)
+ i = 0
+ while i < len(lines) and (lines[i].strip()=="" or lines[i].lstrip().startswith("#!") or re.match(r'^\s*#.*coding[:=]', lines[i])):
+ i += 1
+ insert_at = i
+ if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]): # докстринг
+ q = '"""' if '"""' in lines[i] else "'''"
+ j = i
+ while j < len(lines):
+ if q in lines[j] and j != i:
+ j += 1
+ break
+ j += 1
+ insert_at = min(j, len(lines))
+
+ if not any("from __future__ import annotations" in ln for ln in lines):
+ lines.insert(insert_at, "from __future__ import annotations\n")
+ changed = True
+
+ txt = "".join(lines)
+ if "from uuid import UUID" not in txt:
+ # вставим сразу после future
+ # найдём позицию future
+ pos = next((k for k,ln in enumerate(lines) if "from __future__ import annotations" in ln), 0)
+ lines.insert(pos+1, "from uuid import UUID\n")
+ changed = True
+
+ return "".join(lines), changed
+
+def set_config(text: str) -> str:
+ # Pydantic v2: добавим ConfigDict и model_config, если их нет
+ if "ConfigDict" not in text:
+ text = re.sub(r'from pydantic import ([^\n]+)',
+ lambda m: m.group(0).replace(m.group(1), (m.group(1) + ", ConfigDict").replace(", ConfigDict,"," , ConfigDict,")),
+ text, count=1) if "from pydantic import" in text else ("from pydantic import BaseModel, ConfigDict\n" + text)
+
+ # В каждую модель, наследующую BaseModel, которая заканчивается на Read/Out, добавим from_attributes
+ def inject_cfg(block: str) -> str:
+ # если уже есть model_config/orm_mode — не трогаем
+ if "model_config" in block or "class Config" in block:
+ return block
+ # аккуратно добавим model_config сверху класса
+ header, body = block.split(":", 1)
+ return header + ":\n model_config = ConfigDict(from_attributes=True)\n" + body
+
+ # Разобьём по классам
+ parts = re.split(r'(\nclass\s+[A-Za-z0-9_]+\s*\([^\)]*BaseModel[^\)]*\)\s*:)', text)
+ if len(parts) > 1:
+ out = [parts[0]]
+ for i in range(1, len(parts), 2):
+ head = parts[i]
+ body = parts[i+1]
+ m = re.search(r'class\s+([A-Za-z0-9_]+)\s*\(', head)
+ name = m.group(1) if m else ""
+ if name.endswith(("Read","Out")):
+ out.append(inject_cfg(head + body))
+ else:
+ out.append(head + body)
+ text = "".join(out)
+ return text
+
+def fix_uuid_fields(text: str) -> str:
+ # Преобразуем только в классах Read/Out
+ def repl_in_class(cls_text: str) -> str:
+ # заменяем аннотации полей
+ repl = {
+ r'(^\s*)(id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(pair_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(room_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(sender_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ r'(^\s*)(user_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
+ }
+ for pat, sub in repl.items():
+ cls_text = re.sub(pat, sub, cls_text, flags=re.M)
+ return cls_text
+
+ patt = re.compile(r'(\nclass\s+[A-Za-z0-9_]+(Read|Out)\s*\([^\)]*BaseModel[^\)]*\)\s*:\n(?:\s+.*\n)+)', re.M)
+ return patt.sub(lambda m: repl_in_class(m.group(1)), text)
+
+changed_any = False
+for p in root.glob("**/*.py"):
+ txt = p.read_text()
+ new, c1 = ensure_future_and_uuid(txt)
+ new = set_config(new)
+ new2 = fix_uuid_fields(new)
+ if new2 != txt:
+ p.write_text(new2)
+ print("fixed:", p)
+ changed_any = True
+
+print("DONE" if changed_any else "NO-CHANGES")
+PY
+
+echo "[docker] rebuild & restart chat…"
+docker compose build --no-cache chat >/dev/null
+docker compose restart chat
-echo "[gateway] restart..."
-docker compose restart gateway
-BASH
-chmod +x scripts/patch_gateway_auth_header.sh
-./scripts/patch_gateway_auth_header.sh
diff --git a/services/auth/src/app/schemas/__init__.py b/services/auth/src/app/schemas/__init__.py
index e69de29..9d48db4 100644
--- a/services/auth/src/app/schemas/__init__.py
+++ b/services/auth/src/app/schemas/__init__.py
@@ -0,0 +1 @@
+from __future__ import annotations
diff --git a/services/auth/src/app/schemas/common.py b/services/auth/src/app/schemas/common.py
index faa7726..c0867ef 100644
--- a/services/auth/src/app/schemas/common.py
+++ b/services/auth/src/app/schemas/common.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
from pydantic import BaseModel
class Message(BaseModel):
diff --git a/services/auth/src/app/schemas/user.py b/services/auth/src/app/schemas/user.py
index 5e69bed..d3b6677 100644
--- a/services/auth/src/app/schemas/user.py
+++ b/services/auth/src/app/schemas/user.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+from uuid import UUID
from typing import Optional
from pydantic import BaseModel, EmailStr, ConfigDict
@@ -21,7 +22,7 @@ class UserUpdate(BaseModel):
password: Optional[str] = None
class UserRead(BaseModel):
- id: str
+ id: UUID
email: EmailStr
full_name: Optional[str] = None
role: str
diff --git a/services/chat/src/app/schemas/__init__.py b/services/chat/src/app/schemas/__init__.py
index e69de29..4f6e74c 100644
--- a/services/chat/src/app/schemas/__init__.py
+++ b/services/chat/src/app/schemas/__init__.py
@@ -0,0 +1,3 @@
+from __future__ import annotations
+from pydantic import BaseModel, ConfigDict
+from uuid import UUID
diff --git a/services/chat/src/app/schemas/chat.py b/services/chat/src/app/schemas/chat.py
index 48b35a2..9b79d1e 100644
--- a/services/chat/src/app/schemas/chat.py
+++ b/services/chat/src/app/schemas/chat.py
@@ -1,4 +1,5 @@
from __future__ import annotations
+from uuid import UUID
from pydantic import BaseModel, ConfigDict
from typing import Optional
@@ -7,7 +8,7 @@ class RoomCreate(BaseModel):
participants: list[str] # user IDs
class RoomRead(BaseModel):
- id: str
+ id: UUID
title: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@@ -15,8 +16,8 @@ class MessageCreate(BaseModel):
content: str
class MessageRead(BaseModel):
- id: str
- room_id: str
- sender_id: str
+ id: UUID
+ room_id: UUID
+ sender_id: UUID
content: str
model_config = ConfigDict(from_attributes=True)
diff --git a/services/chat/src/app/schemas/common.py b/services/chat/src/app/schemas/common.py
index faa7726..e379fd8 100644
--- a/services/chat/src/app/schemas/common.py
+++ b/services/chat/src/app/schemas/common.py
@@ -1,4 +1,6 @@
-from pydantic import BaseModel
+from __future__ import annotations
+from uuid import UUID
+from pydantic import BaseModel, ConfigDict
class Message(BaseModel):
message: str
diff --git a/services/match/src/app/schemas/__init__.py b/services/match/src/app/schemas/__init__.py
index e69de29..9d48db4 100644
--- a/services/match/src/app/schemas/__init__.py
+++ b/services/match/src/app/schemas/__init__.py
@@ -0,0 +1 @@
+from __future__ import annotations
diff --git a/services/match/src/app/schemas/common.py b/services/match/src/app/schemas/common.py
index faa7726..424302e 100644
--- a/services/match/src/app/schemas/common.py
+++ b/services/match/src/app/schemas/common.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+from uuid import UUID
from pydantic import BaseModel
class Message(BaseModel):
diff --git a/services/match/src/app/schemas/pair.py b/services/match/src/app/schemas/pair.py
index 310798a..4c93398 100644
--- a/services/match/src/app/schemas/pair.py
+++ b/services/match/src/app/schemas/pair.py
@@ -1,10 +1,11 @@
from __future__ import annotations
+from uuid import UUID
from typing import Optional
from pydantic import BaseModel, ConfigDict
class PairCreate(BaseModel):
- user_id_a: str
- user_id_b: str
+ user_id_a: UUID
+ user_id_b: UUID
score: Optional[float] = None
notes: Optional[str] = None
@@ -13,9 +14,9 @@ class PairUpdate(BaseModel):
notes: Optional[str] = None
class PairRead(BaseModel):
- id: str
- user_id_a: str
- user_id_b: str
+ id: UUID
+ user_id_a: UUID
+ user_id_b: UUID
status: str
score: Optional[float] = None
notes: Optional[str] = None
diff --git a/services/profiles/src/app/schemas/__init__.py b/services/profiles/src/app/schemas/__init__.py
index e69de29..9d48db4 100644
--- a/services/profiles/src/app/schemas/__init__.py
+++ b/services/profiles/src/app/schemas/__init__.py
@@ -0,0 +1 @@
+from __future__ import annotations
diff --git a/services/profiles/src/app/schemas/common.py b/services/profiles/src/app/schemas/common.py
index faa7726..c0867ef 100644
--- a/services/profiles/src/app/schemas/common.py
+++ b/services/profiles/src/app/schemas/common.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
from pydantic import BaseModel
class Message(BaseModel):
diff --git a/services/profiles/src/app/schemas/profile.py b/services/profiles/src/app/schemas/profile.py
index 55d6123..77c7606 100644
--- a/services/profiles/src/app/schemas/profile.py
+++ b/services/profiles/src/app/schemas/profile.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import List
from uuid import UUID
+from typing import List
try:
# Pydantic v2