refactor logging module, add welcome and reset daemons, changed structure of the rabbitmq queues

This commit is contained in:
2026-09-13 18:40:14 +03:00
parent b4e88a6ff4
commit a93c6d5fca
20 changed files with 374 additions and 58 deletions
+7
View File
@@ -0,0 +1,7 @@
---
name: code-reviewer
description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You are a senior code reviewer. Read the recent git diff or modified files, then report only what matters: correctness bugs, security vulnerabilities, and maintainability traps. Lead with the highest-severity finding.
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"sandbox": {
"filesystem": {
"allowRead": ["."],
"denyRead": ["**/.env", "./DB", "./RTMQ", "./.vscode", "./.pytest_cache", "**/__pycache__"]
}
}
}
+7 -4
View File
@@ -17,14 +17,17 @@ __pycache__/
.DS_Store
Thumbs.db
#env
# env files
*.env
#db
# DB
*.db
DB/
#rabbitmq
# rabbitmq
RTMQ/
#logs
# logs
logs/
#Примеры документов
+7
View File
@@ -16,7 +16,14 @@ REDIS_HOST="change_me"
RABBITMQ_PASSWORD="change_me"
RABBITMQ_LOGIN="change_me"
RABBITMQ_HOST="change_me"
RABBITMQ_PORT=change_me #int
RABBITMQ_PORT_UI=chane_me #int
EMAIL_PORT=change_me #int
SMTP_SERVER="change_me"
EMAIL_LOGIN="change_me"
EMAIL_PASSWORD="change_me"
PROD_MODE=bool
+5
View File
@@ -3,7 +3,10 @@ import os
import sys
from src.daemons.registry import DAEMONS
from src.messaging.rabbitmq_client import rabbitmq_client
from src.messaging.topology_setup import apply_topology
from src.models.configs_read.daemons_json import daemons_config
from src.models.rabbitmq_models.email import email_topology
async def run_one(daemon_name: str) -> None:
@@ -34,6 +37,8 @@ async def run_enabled_from_config() -> None:
async def main() -> None:
channel = await rabbitmq_client.get_channel()
await apply_topology(channel, email_topology)
if len(sys.argv) < 2:
print("Usage: python run_daemon.py <daemon_name> | --all")
sys.exit(1)
+9 -1
View File
@@ -1,3 +1,4 @@
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
@@ -5,19 +6,26 @@ from fastapi import FastAPI
from src.cache.redis_client import redis_client
from src.database.users.crud import Seed
from src.logging.logger import LoggingMiddleware, ProcessingTimeMiddleware
from src.logging.logger import LoggingMiddleware, LogWriter, ProcessingTimeMiddleware
from src.messaging.rabbitmq_client import rabbitmq_client
from src.messaging.topology_setup import apply_topology
from src.models.rabbitmq_models.email import email_topology
from src.web.protected_routes.auth_routes import router as protected_router
from src.web.protected_routes.protected_user_action_routes import (
router as protected_user_action_routes,
)
writer=LogWriter()
@asynccontextmanager
async def lifespan(app: FastAPI):
create_dirs()
channel = await rabbitmq_client.get_channel()
await apply_topology(channel, email_topology)
await create_first_user()
writer_task=asyncio.create_task(writer.log_writer())
yield
writer_task.cancel()
await redis_client.close()
await rabbitmq_client.close()
Generated
+49 -3
View File
@@ -44,6 +44,22 @@ files = [
pamqp = ">=4,<5"
yarl = "*"
[[package]]
name = "aiosmtpd"
version = "1.4.6"
description = "aiosmtpd - asyncio based SMTP server"
optional = false
python-versions = ">=3.8"
groups = ["daemon"]
files = [
{file = "aiosmtpd-1.4.6-py3-none-any.whl", hash = "sha256:72c99179ba5aa9ae0abbda6994668239b64a5ce054471955fe75f581d2592475"},
{file = "aiosmtpd-1.4.6.tar.gz", hash = "sha256:5a811826e1a5a06c25ebc3e6c4a704613eb9a1bcf6b78428fbe865f4f6c9a4b8"},
]
[package.dependencies]
atpublic = "*"
attrs = "*"
[[package]]
name = "alembic"
version = "1.18.5"
@@ -224,13 +240,25 @@ files = [
[package.extras]
gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""]
[[package]]
name = "atpublic"
version = "7.0.0"
description = "Keep all y'all's __all__'s in sync"
optional = false
python-versions = ">=3.10"
groups = ["daemon"]
files = [
{file = "atpublic-7.0.0-py3-none-any.whl", hash = "sha256:6702bd9e7245eb4e8220a3e222afcef7f87412154732271ee7deee4433b72b4b"},
{file = "atpublic-7.0.0.tar.gz", hash = "sha256:466ef10d0c8bbd14fd02a5fbd5a8b6af6a846373d91106d3a07c16d72d96b63e"},
]
[[package]]
name = "attrs"
version = "26.1.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
groups = ["daemon", "dev"]
files = [
{file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"},
{file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"},
@@ -977,6 +1005,24 @@ parso = ">=0.8.6,<0.9.0"
dev = ["Django", "attrs", "colorama", "docopt", "flake8 (==7.1.2)", "pytest (<9.0.0)", "types-setuptools (==80.9.0.20250529)", "typing-extensions", "zuban (==0.7.0)"]
docs = ["Jinja2 (==3.1.6)", "MarkupSafe (==3.0.3)", "Pygments (==2.20.0)", "Sphinx (==9.1.0)", "alabaster (==1.0.0)", "babel (==2.18.0)", "certifi (==2026.4.22)", "charset-normalizer (==3.4.7)", "docutils (==0.22.4)", "idna (==3.13)", "imagesize (==2.0.0)", "iniconfig (==2.3.0)", "packaging (==26.2)", "pluggy (==1.6.0)", "pytest (==9.0.3)", "requests (==2.33.1)", "roman-numerals (==4.1.0)", "snowballstemmer (==3.0.1)", "sphinx-rtd-theme (==3.1.0)", "sphinxcontrib-applehelp (==2.0.0)", "sphinxcontrib-devhelp (==2.0.0)", "sphinxcontrib-htmlhelp (==2.1.0)", "sphinxcontrib-jquery (==4.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==2.0.0)", "sphinxcontrib-serializinghtml (==2.0.0)", "urllib3 (==2.6.3)"]
[[package]]
name = "jinja2"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["daemon"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "mako"
version = "1.3.12"
@@ -1027,7 +1073,7 @@ version = "3.0.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "daemon"]
files = [
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
@@ -2527,4 +2573,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = ">=3.13,<4.0"
content-hash = "025191389a53d34b4a8430d8ab7afde489ccd7692f5e0e8e2d29b8c26aa94364"
content-hash = "2ac8d07b41e04495f16ac9da7093f96ec08d094402ebafe48a421a40a98c7d05"
+3
View File
@@ -22,6 +22,7 @@ dependencies = [
"psycopg2-binary (>=2.9.12,<3.0.0)",
"redis (>=8.1.0,<9.0.0)",
"aio-pika (>=10.0.1,<11.0.0)",
]
[tool.poetry.group.web.dependencies]
@@ -41,6 +42,8 @@ pytest-asyncio = ">=1.4.0,<2.0.0"
requests-async = ">=0.2.4,<0.3.0"
[tool.poetry.group.daemon.dependencies]
aiosmtpd = ">=1.4.6,<2.0.0"
jinja2 = ">=3.1.6,<4.0.0"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
+46 -24
View File
@@ -1,8 +1,10 @@
import asyncio
import json
import logging
from contextvars import ContextVar
from time import gmtime, perf_counter, strftime
from typing import cast
from uuid import uuid4
import aiofiles
from fastapi import Request
@@ -11,8 +13,28 @@ from starlette.concurrency import iterate_in_threadpool
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, StreamingResponse
request_id_ctx: ContextVar[str] = ContextVar("request_id", default="-")
class ProcessingTimeMiddleware(BaseHTTPMiddleware):
log_queue=asyncio.Queue()
class LogWriter:
def __init__(self) -> None:
pass
async def log_writer(self)->None:
while True:
log_type, msg=await log_queue.get()
await self._write(msg, log_type)
async def _write(self, msg:str, type:str)->None:
file_time = strftime("%b_%Y", gmtime())
current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
async with aiofiles.open(f"./logs/{type}_log_{file_time}.txt", "a") as f:
await f.write(f"[{current_time}] {msg}\n")
class ProcessingTimeMiddleware(BaseHTTPMiddleware, ):
async def dispatch(self, request: Request, call_next)->Response:
start_time = perf_counter()
response = await call_next(request)
@@ -22,19 +44,26 @@ class ProcessingTimeMiddleware(BaseHTTPMiddleware):
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
file_time = strftime("%b_%Y", gmtime())
client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown')
async def build_log_line(self, request_id, method, path, status_code, detail, client_ip) -> str:
return f"[{request_id}] [{method}] [{path}] [{status_code}] [{detail}] [{client_ip}]"
async def dispatch(self, request: Request, call_next) -> Response:
request_id=str(uuid4())
request_id_ctx.set(request_id)
client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown')
method = request.method
path=request.url.path
try:
response = await call_next(request)
except Exception as exc: # noqa: BLE001
body = str(exc)
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
await file.write(f"[{current_time}] [500] [{body}] [{client_ip}]\n")
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
line=await self.build_log_line(request_id=request_id, method=method, path=path, status_code=500, detail=repr(exc), client_ip=client_ip)
log_queue.put_nowait(("endpoints", line))
return JSONResponse(
status_code=500,
content={"detail": "Internal Server Error", "request_id": request_id}
)
streaming_response = cast(StreamingResponse, response)
chunks = []
@@ -49,23 +78,16 @@ class LoggingMiddleware(BaseHTTPMiddleware):
body = parsed.get("detail", None) if not isinstance(parsed, bool) else None
except (json.JSONDecodeError, TypeError):
body = None
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
await file.write(f"[{current_time}] [{response.status_code}] [{body}] [{client_ip}]\n")
line=await self.build_log_line(request_id=request_id, method=method, path=path, status_code=response.status_code, detail=body, client_ip=client_ip)
log_queue.put_nowait(("endpoints", line))
return response
class LoggerDB(logging.Handler):
class LoggerDB(logging.Handler, LogWriter):
def emit(self, record: logging.LogRecord) -> None:
msg = self.format(record)
asyncio.create_task(self._write(msg))
async def _write(self, msg: str) -> None:
file_time = strftime("%b_%Y", gmtime())
current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
async with aiofiles.open(f"./logs/sql_log_{file_time}.txt", "a") as f:
await f.write(f"[{current_time}] {msg}\n")
rid=request_id_ctx.get()
log_queue.put_nowait(("sql",f"[{rid}] {msg}"))
+25 -17
View File
@@ -1,8 +1,8 @@
import json
import aio_pika
import smtplib
from src.messaging.rabbitmq_client import rabbitmq_client
from src.service.email.email_welcome import DaemonEmailSender
class WelcomeEmailConsumer:
@@ -10,22 +10,33 @@ class WelcomeEmailConsumer:
def __init__(self) -> None:
self.channel = None
self.queue = None
async def setup(self)->None:
self.daemon=DaemonEmailSender()
async def setup(self) -> None:
self.channel = await rabbitmq_client.get_channel()
await self.channel.set_qos(prefetch_count=10)
exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC)
self.queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"})
await self.queue.bind(exchange, routing_key="email.welcome")
self.queue = await self.channel.get_queue("queue_welcome_email")
async def process_message(self, message) -> None:
async with message.process():
async with message.process(ignore_processed=True):
data = json.loads(message.body)
print(f"Обрабатываю: {data}, метка: {message.routing_key}")
try:
await self.daemon.send_email(data.get("email"))
except (
smtplib.SMTPServerDisconnected,
smtplib.SMTPConnectError,
TimeoutError,
ConnectionRefusedError,
) as exc:
print(f"transient error, retrying: {exc!r}")
await message.nack(requeue=True)
except Exception as exc: # noqa: BLE001
print(f"permanent error, sending to DLQ: {exc!r}")
await message.nack(requeue=False)
async def start_consuming(self)->None:
@@ -46,15 +57,12 @@ class ResetEmailConsumer:
self.channel = None
self.queue = None
async def setup(self)->None:
async def setup(self) -> None:
self.channel = await rabbitmq_client.get_channel()
await self.channel.set_qos(prefetch_count=10)
exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC)
self.queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"})
await self.queue.bind(exchange, routing_key="email.reset")
self.queue = await self.channel.get_queue("queue_reset_email")
async def process_message(self, message) -> None:
+2 -8
View File
@@ -11,16 +11,10 @@ class EmailProducer:
self.channel = None
self.exchange = None
async def setup(self)->None:
async def setup(self) -> None:
self.channel = await rabbitmq_client.get_channel()
self.exchange = await self.channel.declare_exchange("email", aio_pika.ExchangeType.TOPIC)
welcome_queue = await self.channel.declare_queue("queue_welcome_email", durable=True, arguments={"x-queue-type": "quorum"})
await welcome_queue.bind(self.exchange, routing_key="email.welcome")
self.exchange = await self.channel.get_exchange("email")
reset_queue = await self.channel.declare_queue("queue_reset_email", durable=True, arguments={"x-queue-type": "quorum"})
await reset_queue.bind(self.exchange, routing_key="email.reset")
async def send_welcome_email(self, email:str)->None:
+22
View File
@@ -0,0 +1,22 @@
from src.models.rabbitmq_models.email import Topology
async def apply_topology(channel, topology: Topology) -> None:
dlx = await channel.declare_exchange("email.dlx", "direct")
for exch_spec in topology.exchanges:
exchange = await channel.declare_exchange(exch_spec.name, exch_spec.type)
for q in exch_spec.queues:
queue = await channel.declare_queue(
q.name,
durable=True,
arguments={
"x-queue-type": "quorum",
"x-dead-letter-exchange": "email.dlx",
},
)
await queue.bind(exchange, routing_key=q.routing_key)
if q.dlx_queue:
dlq = await channel.declare_queue(q.dlx_queue, durable=True)
await dlq.bind(dlx, routing_key=q.routing_key)
+5
View File
@@ -26,6 +26,11 @@ class Env(Base):
RABBITMQ_PORT:int
RABBITMQ_PORT_UI:int
EMAIL_PORT:int
SMTP_SERVER:str
EMAIL_LOGIN:str
EMAIL_PASSWORD:str
PROD_MODE:bool
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
+24
View File
@@ -0,0 +1,24 @@
from pydantic import BaseModel
class QueueSpec(BaseModel):
name:str
routing_key:str
dlx_queue:str|None = None
class ExchangeSpec(BaseModel):
name:str
type:str = "topic"
queues: list[QueueSpec]
class Topology(BaseModel):
exchanges:list[ExchangeSpec]
email_topology=Topology(exchanges=[
ExchangeSpec(name="email", queues=[
QueueSpec(name="queue_welcome_email", routing_key="email.welcome", dlx_queue="queue_welcome_email.dlq"),
QueueSpec(name="queue_reset_email", routing_key="email.reset", dlx_queue="queue_reset_email.dlq"),
]),
])
View File
+40
View File
@@ -0,0 +1,40 @@
import smtplib
import ssl
from email.message import EmailMessage
from src.models.configs_read.env import env_settings
from src.service.email.jinja_env import jinja_env
class DaemonEmailSender:
def __init__(self) -> None:
pass
async def send_email(self, target_email:str)->None:
context = ssl.create_default_context()
with smtplib.SMTP_SSL(
env_settings.SMTP_SERVER, env_settings.EMAIL_PORT, context=context
) as server:
server.login(env_settings.EMAIL_LOGIN, env_settings.EMAIL_PASSWORD)
msg=await self.build_email_message(target_email)
server.send_message(msg)
async def build_email_message(self, target_email: str) -> EmailMessage:
template = jinja_env.get_template("welcome.html")
html_body = template.render(name=target_email)
text_body = (
f"Добро пожаловать, {target_email}!\n\n"
"Спасибо за регистрацию на проекте «The DisExcel». "
"Мы рады, что вы с нами — учётная запись уже готова к работе.\n\n"
"Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо."
)
msg = EmailMessage()
msg["to"] = target_email
msg["from"] = env_settings.EMAIL_LOGIN
msg["subject"] = "Добро пожаловать"
msg.set_content(text_body)
msg.add_alternative(html_body, subtype="html")
return msg
+6
View File
@@ -0,0 +1,6 @@
from jinja2 import Environment, FileSystemLoader
jinja_env = Environment(
loader=FileSystemLoader("src/service/email/templates"),
autoescape=True,
)
+55
View File
@@ -0,0 +1,55 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Пароль сброшен</title>
</head>
<body style="margin:0; padding:0; background-color:#f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding:40px 0;">
<tr>
<td align="center">
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.06);">
<tr>
<td style="background-color:#dc2626; padding:32px 40px; text-align:center;">
<h1 style="margin:0; color:#ffffff; font-size:22px; font-weight:600;">The DisExcel</h1>
</td>
</tr>
<tr>
<td style="padding:40px;">
<h2 style="margin:0 0 16px 0; color:#111827; font-size:20px;">Пароль был сброшен</h2>
<p style="margin:0 0 24px 0; color:#4b5563; font-size:15px; line-height:1.6;">
Пароль от вашей учётной записи в «The DisExcel» был сброшен. Ниже — временный пароль для входа. Рекомендуем сменить его на свой сразу после входа в аккаунт.
</p>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f9fafb; border:1px solid #e5e7eb; border-radius:8px;">
<tr>
<td style="padding:20px; text-align:center;">
<span style="display:block; margin:0 0 4px 0; color:#9ca3af; font-size:12px; text-transform:uppercase; letter-spacing:0.05em;">Новый пароль</span>
<span style="display:inline-block; font-family: 'Courier New', monospace; font-size:20px; font-weight:700; color:#111827; letter-spacing:0.05em;">Тест1234!</span>
</td>
</tr>
</table>
<p style="margin:24px 0 0 0; color:#9ca3af; font-size:13px; line-height:1.5;">
Если вы не запрашивали сброс пароля, срочно свяжитесь с поддержкой — возможно, кто-то получил доступ к вашей учётной записи.
</p>
</td>
</tr>
<tr>
<td style="padding:24px 40px; background-color:#f9fafb; border-top:1px solid #e5e7eb;">
<p style="margin:0; color:#9ca3af; font-size:12px; line-height:1.5;">
Это автоматическое уведомление от The DisExcel. Не пересылайте это письмо никому.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+52
View File
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Добро пожаловать</title>
</head>
<body style="margin:0; padding:0; background-color:#f4f4f7; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f7; padding:40px 0;">
<tr>
<td align="center">
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 2px 8px rgba(0,0,0,0.06);">
<tr>
<td style="background-color:#4f46e5; padding:32px 40px; text-align:center;">
<h1 style="margin:0; color:#ffffff; font-size:22px; font-weight:600;">The DisExcel</h1>
</td>
</tr>
<tr>
<td style="padding:40px;">
<h2 style="margin:0 0 16px 0; color:#111827; font-size:20px;">Добро пожаловать, {{ name }}</h2>
<p style="margin:0 0 24px 0; color:#4b5563; font-size:15px; line-height:1.6;">
Спасибо за регистрацию на проекте «The DisExcel». Мы рады, что вы с нами — учётная запись уже готова к работе.
</p>
<table role="presentation" cellpadding="0" cellspacing="0" align="center" style="margin:0 auto;">
<tr>
<td style="border-radius:8px; background-color:#4f46e5;">
<a href="#" style="display:inline-block; padding:12px 28px; color:#ffffff; font-size:14px; font-weight:600; text-decoration:none;">
Перейти в аккаунт
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:24px 40px; background-color:#f9fafb; border-top:1px solid #e5e7eb;">
<p style="margin:0; color:#9ca3af; font-size:12px; line-height:1.5;">
Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+1 -1
View File
@@ -79,5 +79,5 @@ from src.messaging.producers.producers import email_producer
@router.get("")
async def protected(current_user:UserOut=Depends(require_permissions()))->dict:
await email_producer.send_welcome_email("test@test.com")
await email_producer.send_welcome_email(current_user.email)
return {"protected router": "Hello, this is a protected router"}