dev #16
@@ -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.
|
||||
@@ -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
@@ -17,14 +17,17 @@ __pycache__/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#env
|
||||
# env files
|
||||
*.env
|
||||
#db
|
||||
|
||||
# DB
|
||||
*.db
|
||||
DB/
|
||||
#rabbitmq
|
||||
|
||||
# rabbitmq
|
||||
RTMQ/
|
||||
#logs
|
||||
|
||||
# logs
|
||||
logs/
|
||||
|
||||
#Примеры документов
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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}"))
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
]),
|
||||
])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader("src/service/email/templates"),
|
||||
autoescape=True,
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user