From a93c6d5fca134e492518196a0974e1ab8be3ec54 Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Sun, 13 Sep 2026 18:40:14 +0300 Subject: [PATCH] refactor logging module, add welcome and reset daemons, changed structure of the rabbitmq queues --- .claude/agents/code-reviewer.md | 7 +++ .claude/settings.local.json | 9 +++ .gitignore | 11 ++-- configs/.env.example | 7 +++ daemon_run.py | 5 ++ main.py | 10 +++- poetry.lock | 52 +++++++++++++++++- pyproject.toml | 3 + src/logging/logger.py | 70 ++++++++++++++++-------- src/messaging/consumers/consumers.py | 42 ++++++++------ src/messaging/producers/producers.py | 10 +--- src/messaging/topology_setup.py | 22 ++++++++ src/models/configs_read/env.py | 5 ++ src/models/rabbitmq_models/email.py | 24 ++++++++ src/service/email/email_reset.py | 0 src/service/email/email_welcome.py | 40 ++++++++++++++ src/service/email/jinja_env.py | 6 ++ src/service/email/templates/reset.html | 55 +++++++++++++++++++ src/service/email/templates/welcome.html | 52 ++++++++++++++++++ src/web/protected_routes/auth_routes.py | 2 +- 20 files changed, 374 insertions(+), 58 deletions(-) create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/settings.local.json create mode 100644 src/messaging/topology_setup.py create mode 100644 src/models/rabbitmq_models/email.py create mode 100644 src/service/email/email_reset.py create mode 100644 src/service/email/email_welcome.py create mode 100644 src/service/email/jinja_env.py create mode 100644 src/service/email/templates/reset.html create mode 100644 src/service/email/templates/welcome.html diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..9c5308b --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -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. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..16d24f5 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "sandbox": { + "filesystem": { + "allowRead": ["."], + "denyRead": ["**/.env", "./DB", "./RTMQ", "./.vscode", "./.pytest_cache", "**/__pycache__"] + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 41df06c..4f00b33 100644 --- a/.gitignore +++ b/.gitignore @@ -17,14 +17,17 @@ __pycache__/ .DS_Store Thumbs.db -#env +# env files *.env -#db + +# DB *.db DB/ -#rabbitmq + +# rabbitmq RTMQ/ -#logs + +# logs logs/ #Примеры документов diff --git a/configs/.env.example b/configs/.env.example index 27b7172..b4875b3 100644 --- a/configs/.env.example +++ b/configs/.env.example @@ -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 \ No newline at end of file diff --git a/daemon_run.py b/daemon_run.py index 0fcf912..e04b2c2 100644 --- a/daemon_run.py +++ b/daemon_run.py @@ -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 | --all") sys.exit(1) diff --git a/main.py b/main.py index 1bdc874..c3ffcac 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/poetry.lock b/poetry.lock index 2ef8ecb..c469b1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 481f030..0e60d68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/logging/logger.py b/src/logging/logger.py index c270942..7e0ffbd 100644 --- a/src/logging/logger.py +++ b/src/logging/logger.py @@ -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}")) \ No newline at end of file diff --git a/src/messaging/consumers/consumers.py b/src/messaging/consumers/consumers.py index 068502d..974f771 100644 --- a/src/messaging/consumers/consumers.py +++ b/src/messaging/consumers/consumers.py @@ -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: diff --git a/src/messaging/producers/producers.py b/src/messaging/producers/producers.py index 8179f4c..157c9e3 100644 --- a/src/messaging/producers/producers.py +++ b/src/messaging/producers/producers.py @@ -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: diff --git a/src/messaging/topology_setup.py b/src/messaging/topology_setup.py new file mode 100644 index 0000000..e31bdb0 --- /dev/null +++ b/src/messaging/topology_setup.py @@ -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) diff --git a/src/models/configs_read/env.py b/src/models/configs_read/env.py index 29002de..25ecfe3 100644 --- a/src/models/configs_read/env.py +++ b/src/models/configs_read/env.py @@ -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) diff --git a/src/models/rabbitmq_models/email.py b/src/models/rabbitmq_models/email.py new file mode 100644 index 0000000..731b982 --- /dev/null +++ b/src/models/rabbitmq_models/email.py @@ -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"), + ]), +]) + + \ No newline at end of file diff --git a/src/service/email/email_reset.py b/src/service/email/email_reset.py new file mode 100644 index 0000000..e69de29 diff --git a/src/service/email/email_welcome.py b/src/service/email/email_welcome.py new file mode 100644 index 0000000..6f076f5 --- /dev/null +++ b/src/service/email/email_welcome.py @@ -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 + diff --git a/src/service/email/jinja_env.py b/src/service/email/jinja_env.py new file mode 100644 index 0000000..b5a4d59 --- /dev/null +++ b/src/service/email/jinja_env.py @@ -0,0 +1,6 @@ +from jinja2 import Environment, FileSystemLoader + +jinja_env = Environment( + loader=FileSystemLoader("src/service/email/templates"), + autoescape=True, +) \ No newline at end of file diff --git a/src/service/email/templates/reset.html b/src/service/email/templates/reset.html new file mode 100644 index 0000000..55eb806 --- /dev/null +++ b/src/service/email/templates/reset.html @@ -0,0 +1,55 @@ + + + + + + Пароль сброшен + + + + + + +
+ + + + + + + + + + + + + + +
+

The DisExcel

+
+

Пароль был сброшен

+

+ Пароль от вашей учётной записи в «The DisExcel» был сброшен. Ниже — временный пароль для входа. Рекомендуем сменить его на свой сразу после входа в аккаунт. +

+ + + + + +
+ Новый пароль + Тест1234! +
+ +

+ Если вы не запрашивали сброс пароля, срочно свяжитесь с поддержкой — возможно, кто-то получил доступ к вашей учётной записи. +

+
+

+ Это автоматическое уведомление от The DisExcel. Не пересылайте это письмо никому. +

+
+
+ + \ No newline at end of file diff --git a/src/service/email/templates/welcome.html b/src/service/email/templates/welcome.html new file mode 100644 index 0000000..f66eff2 --- /dev/null +++ b/src/service/email/templates/welcome.html @@ -0,0 +1,52 @@ + + + + + + Добро пожаловать + + + + + + +
+ + + + + + + + + + + + + + +
+

The DisExcel

+
+

Добро пожаловать, {{ name }}

+

+ Спасибо за регистрацию на проекте «The DisExcel». Мы рады, что вы с нами — учётная запись уже готова к работе. +

+ + + + + +
+ + Перейти в аккаунт + +
+
+

+ Если вы не регистрировались на The DisExcel, просто проигнорируйте это письмо. +

+
+
+ + \ No newline at end of file diff --git a/src/web/protected_routes/auth_routes.py b/src/web/protected_routes/auth_routes.py index 6d33990..92b6419 100644 --- a/src/web/protected_routes/auth_routes.py +++ b/src/web/protected_routes/auth_routes.py @@ -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"}