From aaf68412e241353792f8bde0d28400e2b9f01da5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 15:21:31 +0300 Subject: [PATCH 01/31] add methods for send data to api for recovery password --- frontend/src/services/auth.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 5262059..451fadc 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -34,6 +34,23 @@ class AuthService { return response.data } + async forgotPassword(email) { + const response = await axios.post(`api/auth/forgot-password`, { + email, + }) + + return response.data + } + + async resetPassword(token, password) { + const response = await axios.post(`api/auth/reset-password`, { + token, + password, + }) + + return response.data + } + async fetchUser() { if (!this.token) return null From 7921ff9f4c29f71072004bf4916d5891224bed00 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 15:21:47 +0300 Subject: [PATCH 02/31] add new routers --- frontend/src/router/index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 9871919..00c3b61 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -4,6 +4,8 @@ import authService from '../services/auth' import HomeView from '../components/Home.vue' import LoginView from '../components/Login.vue' import RegisterView from '../components/Register.vue' +import ForgotPasswordView from '../components/ForgotPassword.vue' +import ResetPasswordView from '../components/ResetPassword.vue' const routes = [ { @@ -24,6 +26,18 @@ const routes = [ component: RegisterView, meta: { guest: true }, }, + { + path: '/forgot-password', + name: 'ForgotPassword', + component: ForgotPasswordView, + meta: { guest: true }, + }, + { + path: '/reset-password', + name: 'ResetPassword', + component: ResetPasswordView, + meta: { guest: true }, + }, ] const router = createRouter({ @@ -31,7 +45,6 @@ const router = createRouter({ routes, }) -// Navigation guard router.beforeEach((to, from, next) => { const isAuthenticated = authService.isAuthenticated() From 1a82d241e007525c1b7f572402d0e4db0bcdeb88 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 15:22:25 +0300 Subject: [PATCH 03/31] add new page for reset password --- frontend/src/components/ForgotPassword.vue | 99 ++++++++++++++++ frontend/src/components/ResetPassword.vue | 132 +++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 frontend/src/components/ForgotPassword.vue create mode 100644 frontend/src/components/ResetPassword.vue diff --git a/frontend/src/components/ForgotPassword.vue b/frontend/src/components/ForgotPassword.vue new file mode 100644 index 0000000..32b8ce9 --- /dev/null +++ b/frontend/src/components/ForgotPassword.vue @@ -0,0 +1,99 @@ + + + diff --git a/frontend/src/components/ResetPassword.vue b/frontend/src/components/ResetPassword.vue new file mode 100644 index 0000000..8d949aa --- /dev/null +++ b/frontend/src/components/ResetPassword.vue @@ -0,0 +1,132 @@ + + + From b0c23e036e0b01b2aff3c7344ca67122a08b450b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 15:23:39 +0300 Subject: [PATCH 04/31] user login page has been changed to navigate to the "forgot password" page --- frontend/src/components/Login.vue | 48 +++++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index 2a4389f..dd2d29f 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -18,14 +18,14 @@ Email @@ -35,17 +35,27 @@ Password + +
+ + Forgot password? + +
+

{{ error }}

@@ -53,9 +63,9 @@ From ed29796ad3e3b040c21113ee516473e7cbc2e22f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 16:48:23 +0300 Subject: [PATCH 05/31] install rabbitmq broker --- backend/pyproject.toml | 1 + backend/uv.lock | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 53a4495..260a76b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pydantic-settings[yaml]>=2.12.0", "sqlalchemy[asyncio]>=2.0.46", "taskiq>=0.12.1", + "taskiq-aio-pika>=0.5.0", "taskiq-redis>=1.2.1", "uvicorn>=0.40.0", ] diff --git a/backend/uv.lock b/backend/uv.lock index 46e547e..c706ac4 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -6,6 +6,19 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[[package]] +name = "aio-pika" +version = "9.5.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiormq" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/73/8d1020683970de5532b3b01732d75c8bf922a6505fcdad1a9c7c6405242a/aio_pika-9.5.8.tar.gz", hash = "sha256:7c36874115f522bbe7486c46d8dd711a4dbedd67c4e8a8c47efe593d01862c62", size = 47408, upload-time = "2025-11-12T10:37:10.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/91/513971861d845d28160ecb205ae2cfaf618b16918a9cd4e0b832b5360ce7/aio_pika-9.5.8-py3-none-any.whl", hash = "sha256:f4c6cb8a6c5176d00f39fd7431e9702e638449bc6e86d1769ad7548b2a506a8d", size = 54397, upload-time = "2025-11-12T10:37:08.374Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -83,6 +96,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, ] +[[package]] +name = "aiormq" +version = "6.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pamqp" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/f6/01bc850db6d9b46ae825e3c373f610b0544e725a1159745a6de99ad0d9f1/aiormq-6.9.2.tar.gz", hash = "sha256:d051d46086079934d3a7157f4d8dcb856b77683c2a94aee9faa165efa6a785d3", size = 30554, upload-time = "2025-10-20T10:49:59.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/ec/763b13f148f3760c1562cedb593feaffbae177eeece61af5d0ace7b72a3e/aiormq-6.9.2-py3-none-any.whl", hash = "sha256:ab0f4e88e70f874b0ea344b3c41634d2484b5dc8b17cb6ae0ae7892a172ad003", size = 31829, upload-time = "2025-10-20T10:49:58.547Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -255,6 +281,7 @@ dependencies = [ { name = "pydantic-settings", extra = ["yaml"] }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "taskiq" }, + { name = "taskiq-aio-pika" }, { name = "taskiq-redis" }, { name = "uvicorn" }, ] @@ -282,6 +309,7 @@ requires-dist = [ { name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.12.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.46" }, { name = "taskiq", specifier = ">=0.12.1" }, + { name = "taskiq-aio-pika", specifier = ">=0.5.0" }, { name = "taskiq-redis", specifier = ">=1.2.1" }, { name = "uvicorn", specifier = ">=0.40.0" }, ] @@ -1208,6 +1236,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -1784,6 +1821,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/e4/a2fda3bcbb8b61108dc8e9db1a2d19a23578953db73e981f66b9d44f1207/taskiq-0.12.1-py3-none-any.whl", hash = "sha256:a8ade45e2e23edbadb972a88dec44e68c7daef83383d01fa3af48594a24a712a", size = 90668, upload-time = "2025-12-07T16:07:42.296Z" }, ] +[[package]] +name = "taskiq-aio-pika" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aio-pika" }, + { name = "taskiq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/3c7b7af402df3299f74b6ab5a3e8b6ffbd45b1a9eaab9dce5e4e7b886709/taskiq_aio_pika-0.5.0.tar.gz", hash = "sha256:ded32dc4b9ad97ee6da7b297f393d33aad8f45bb444355ad342f1533e3d161d6", size = 7425, upload-time = "2025-11-25T08:59:14.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/0e/abfdc636b9f0624c2c78bb6e7f33d1673fb01c934c22f76cf91ea82677da/taskiq_aio_pika-0.5.0-py3-none-any.whl", hash = "sha256:bac8c1beaccbdc7af30eebd9c04b17a17e4fd4f30727bc99e4ca464aa2980177", size = 7799, upload-time = "2025-11-25T08:59:13.032Z" }, +] + [[package]] name = "taskiq-dependencies" version = "1.5.7" From a2902b554837e188e7af17a9a1dc88b300e65663 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 16:48:39 +0300 Subject: [PATCH 06/31] add maildev and rabbitmq to docker containers --- docker-compose.dev.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index c072ea7..192bc58 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -20,5 +20,35 @@ services: retries: 5 restart: unless-stopped + maildev: + image: maildev/maildev + environment: + - TZ=Europe/Moscow + - MAILDEV_WEB_PORT=1080 + - MAILDEV_SMTP_PORT=1025 + ports: + - "8080:1080" + - "1025:1025" + logging: + driver: "json-file" + options: + max-size: "1m" + + rabbitmq: + image: rabbitmq:4.2-management-alpine + hostname: rabbitmq + container_name: rabbitmq + ports: + - "5672:5672" + - "15672:15672" + env_file: + - .env + environment: + RABBITMQ_DEFAULT_USER: ${RABBIT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBIT_PSWD} + volumes: + - rabbitmq-data:/var/lib/rabbitmq + volumes: pgdata: + rabbitmq-data: From 9e302c661281b62e82bd9d358a0fb39a58bf5798 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 16:48:49 +0300 Subject: [PATCH 07/31] config for taskiq --- backend/backlog_app/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/backlog_app/config.py b/backend/backlog_app/config.py index e88b236..27d49b8 100644 --- a/backend/backlog_app/config.py +++ b/backend/backlog_app/config.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Literal -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, field_validator, AmqpDsn from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, @@ -51,6 +51,9 @@ def database_url_asyncpg(self): class DataBase(BaseModel): connection: DataBaseConnection +class TaskiqConfig(BaseModel): + url: AmqpDsn = "amqp://guest:guest@localhost:5672//" + class Settings(BaseSettings): model_config = SettingsConfigDict( @@ -99,6 +102,7 @@ def settings_customise_sources( ) db: DataBase + taskiq: TaskiqConfig = TaskiqConfig() logging: LoggingConfig = LoggingConfig() access_token_db: AccessToken superuser: SuperUser From aa2cd230adbbb25bcebf715ac4362bde74d7e1d6 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 16:49:00 +0300 Subject: [PATCH 08/31] use AioPika as broker --- backend/backlog_app/taskiq_broker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/backlog_app/taskiq_broker.py b/backend/backlog_app/taskiq_broker.py index 24f040e..8a9a198 100644 --- a/backend/backlog_app/taskiq_broker.py +++ b/backend/backlog_app/taskiq_broker.py @@ -1,3 +1,4 @@ -from taskiq import InMemoryBroker +from taskiq_aio_pika import AioPikaBroker +from backlog_app.config import settings -broker = InMemoryBroker() +broker = AioPikaBroker(url=settings.taskiq.url) From 5b916bf9f23c61a50d95f1f23361e2ec2326ce26 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 12 Feb 2026 16:49:20 +0300 Subject: [PATCH 09/31] refactor user manager --- .../backlog_app/servicies/authentification/user_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index 289e584..dcb009f 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -6,7 +6,7 @@ from backlog_app.config import settings from backlog_app.models import User -from backlog_app.tasks.email_task import send_email_confirmed, send_verification_email +from backlog_app.tasks import email_task if TYPE_CHECKING: from fastapi import Request @@ -38,7 +38,7 @@ async def on_after_request_verify( verification_link = ( "http://127.0.0.1:8000/docs#/Auth/verify_verify_api_auth_verify_post" ) - await send_verification_email.kiq( + await email_task.send_verification_email.kiq( user_id=str(user.id), user_email=user.email, verification_token=token, @@ -48,7 +48,7 @@ async def on_after_request_verify( async def on_after_verify(self, user: User, request: Optional["Request"] = None): logger.warning("User %s has been verified", user.id) - await send_email_confirmed.kiq( + await email_task.send_email_confirmed.kiq( user_id=str(user.id), user_email=user.email, ) From d50261caacb736b8d187515fe62a95013f940c40 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 13:59:19 +0300 Subject: [PATCH 10/31] install taskiq-fastapi --- backend/pyproject.toml | 1 + backend/uv.lock | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 260a76b..5234588 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "sqlalchemy[asyncio]>=2.0.46", "taskiq>=0.12.1", "taskiq-aio-pika>=0.5.0", + "taskiq-fastapi>=0.4.0", "taskiq-redis>=1.2.1", "uvicorn>=0.40.0", ] diff --git a/backend/uv.lock b/backend/uv.lock index c706ac4..a023510 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -282,6 +282,7 @@ dependencies = [ { name = "sqlalchemy", extra = ["asyncio"] }, { name = "taskiq" }, { name = "taskiq-aio-pika" }, + { name = "taskiq-fastapi" }, { name = "taskiq-redis" }, { name = "uvicorn" }, ] @@ -310,6 +311,7 @@ requires-dist = [ { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.46" }, { name = "taskiq", specifier = ">=0.12.1" }, { name = "taskiq-aio-pika", specifier = ">=0.5.0" }, + { name = "taskiq-fastapi", specifier = ">=0.4.0" }, { name = "taskiq-redis", specifier = ">=1.2.1" }, { name = "uvicorn", specifier = ">=0.40.0" }, ] @@ -1843,6 +1845,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", size = 13801, upload-time = "2025-02-26T22:07:38.622Z" }, ] +[[package]] +name = "taskiq-fastapi" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "taskiq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/fe/f9e885a4dd6bd4d4224e3dee05d9b38bf58102a0ef5e553e812d654dfa72/taskiq_fastapi-0.4.0.tar.gz", hash = "sha256:f683ab0aab1ab2db117d1e121e5e534f3ba4a4e5f382177012e29eca986863fb", size = 5819, upload-time = "2025-11-29T15:22:59.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/58/11eacc48d615703618f93ff44812409dca94cb465ab5cb173a8a90e079b1/taskiq_fastapi-0.4.0-py3-none-any.whl", hash = "sha256:9e7e57fa337b4fad2641c8761cafc7b54a63bf7bf726423a28fc2b15c55fd0bc", size = 5369, upload-time = "2025-11-29T15:23:00.185Z" }, +] + [[package]] name = "taskiq-redis" version = "1.2.1" From 6909fd4382d88da0bf051174e249ad1dbc430754 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 13:59:50 +0300 Subject: [PATCH 11/31] add to logger config taskiq log format --- backend/backlog_app/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/backlog_app/config.py b/backend/backlog_app/config.py index 27d49b8..f40ca79 100644 --- a/backend/backlog_app/config.py +++ b/backend/backlog_app/config.py @@ -17,6 +17,9 @@ class LoggingConfig(BaseModel): log_format: str = ( "[-] %(asctime)s [%(levelname)s] %(module)s-%(lineno)d - %(message)s" ) + worker_log_format: str = ( + "[-] %(asctime)s [%(levelname)s] [%(processName)s] %(module)s-%(lineno)d - %(message)s" + ) log_level_name: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING" log_date_format: str = "%Y-%m-%d %H:%M:%S" From d830225ae758a929c398f249ff30dfae4f6f340a Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 14:00:22 +0300 Subject: [PATCH 12/31] add logging to taskiq --- backend/backlog_app/taskiq_broker.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/backlog_app/taskiq_broker.py b/backend/backlog_app/taskiq_broker.py index 8a9a198..bc06eb5 100644 --- a/backend/backlog_app/taskiq_broker.py +++ b/backend/backlog_app/taskiq_broker.py @@ -1,4 +1,22 @@ + +from taskiq import TaskiqEvents, TaskiqState from taskiq_aio_pika import AioPikaBroker +import taskiq_fastapi from backlog_app.config import settings +import logging + +logger = logging.getLogger(__name__) broker = AioPikaBroker(url=settings.taskiq.url) + +taskiq_fastapi.init(broker, "backlog_app.main:app") + +@broker.on_event(TaskiqEvents.WORKER_STARTUP) +async def on_worker_startup(state: TaskiqState) -> None: + logging.basicConfig( + format=settings.logging.worker_log_format, + level=settings.logging.log_level, + datefmt=settings.logging.log_date_format, + ) + + logger.info("Worker startup complete, got state: %s", state) From d5d5146741e93f5fb84a3e81984273686958c812 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 14:00:46 +0300 Subject: [PATCH 13/31] rename email sender --- .../servicies/mailing/{send_email.py => email_sender.py} | 0 backend/backlog_app/tasks/email_task.py | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename backend/backlog_app/servicies/mailing/{send_email.py => email_sender.py} (100%) diff --git a/backend/backlog_app/servicies/mailing/send_email.py b/backend/backlog_app/servicies/mailing/email_sender.py similarity index 100% rename from backend/backlog_app/servicies/mailing/send_email.py rename to backend/backlog_app/servicies/mailing/email_sender.py diff --git a/backend/backlog_app/tasks/email_task.py b/backend/backlog_app/tasks/email_task.py index e38b8b9..68c3d27 100644 --- a/backend/backlog_app/tasks/email_task.py +++ b/backend/backlog_app/tasks/email_task.py @@ -1,7 +1,7 @@ from textwrap import dedent from backlog_app.jinja2_templates import templates -from backlog_app.servicies.mailing import send_email +from backlog_app.servicies.mailing.email_sender import send_email from backlog_app.taskiq_broker import broker @@ -11,7 +11,7 @@ async def send_verification_email( user_email: str, verification_token: str, verification_link: str, -): +) -> None: subject = "Confirm your email for site.com" plain_content = dedent(f"""\ @@ -26,10 +26,11 @@ async def send_verification_email( template = templates.get_template("email-verify/verification-request.html") context = { "user_id": user_id, + "user_email": user_email, "verification_link": verification_link, "verification_token": verification_token, } - html_content = template.render(context=context) + html_content = template.render(context) await send_email( recipient=user_email, @@ -56,7 +57,7 @@ async def send_email_confirmed( "user_id": user_id, "user_email": user_email, } - html_content = template.render(context=context) + html_content = template.render(context) await send_email( recipient=user_email, From 89492a04e11f6c1d020bb37123ab7a29747b2c73 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 14:01:02 +0300 Subject: [PATCH 14/31] import tasks --- backend/backlog_app/tasks/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/backlog_app/tasks/__init__.py b/backend/backlog_app/tasks/__init__.py index e69de29..b44a03d 100644 --- a/backend/backlog_app/tasks/__init__.py +++ b/backend/backlog_app/tasks/__init__.py @@ -0,0 +1,2 @@ +from .email_task import send_verification_email +from .email_task import send_email_confirmed From 58ea986ed2fe9b860b6ed209c5b05016f8386f31 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 14:01:43 +0300 Subject: [PATCH 15/31] add command for run taskiq --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index ca77612..f8c77a9 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,12 @@ curl -X POST "http://localhost:8000/api/movies" \ - Защищённые эндпоинты требуют заголовок: `Authorization: Bearer ` + +## Брокер сообщений +- Запуск: +```bash +taskiq worker backlog_app.taskiq_broker:broker --fs-discover -tp "**/tasks" --no-configure-logging +``` ## Тестирование - Запуск тестов: From b0f7f249d0307098b23979edcd5741bf6ce3409d Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 14:02:14 +0300 Subject: [PATCH 16/31] edit email template --- backend/backlog_app/templates/base.html | 2 +- .../templates/email-verify/verification-request.html | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/backlog_app/templates/base.html b/backend/backlog_app/templates/base.html index f6e9754..733bd64 100644 --- a/backend/backlog_app/templates/base.html +++ b/backend/backlog_app/templates/base.html @@ -14,7 +14,7 @@ {% block footer %} Your site admin,
- © 2025. + Backlog © 2025. {% endblock %} diff --git a/backend/backlog_app/templates/email-verify/verification-request.html b/backend/backlog_app/templates/email-verify/verification-request.html index de56342..1235950 100644 --- a/backend/backlog_app/templates/email-verify/verification-request.html +++ b/backend/backlog_app/templates/email-verify/verification-request.html @@ -6,11 +6,12 @@ {% block main %}

- Dear, {{ user_email }}, + Dear, {{user_email}}, + Please verify your email for site.com at {{verification_link}}

- To verify your email address, please follow the link: + Use this token to verify your email:
- Confirm email + {{verification_token}}

{% endblock %} From 118a539591390c0de81be0b0ae0606b9409703d8 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 16:01:25 +0300 Subject: [PATCH 17/31] add to auth.js verify email request --- frontend/src/services/auth.js | 215 +++++++++++++++++++--------------- 1 file changed, 118 insertions(+), 97 deletions(-) diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 451fadc..187c5b3 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -1,123 +1,144 @@ import axios from 'axios' class AuthService { - constructor() { - this.token = localStorage.getItem('token') - this.user = JSON.parse(localStorage.getItem('user') || 'null') - } - - async register(email, password) { - const response = await axios.post(`api/auth/register`, { - email, - password, - }) - return response.data - } + constructor() { + this.token = localStorage.getItem('token') + this.user = JSON.parse(localStorage.getItem('user') || 'null') + } - async login(email, password) { - const formData = new URLSearchParams() - formData.append('username', email) - formData.append('password', password) + async register(email, password) { + const response = await axios.post(`/api/auth/register`, { + email, + password, + }) - const response = await axios.post(`api/auth/login`, formData, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }) + await axios.post(`/api/auth/request-verify-token`, { + email + }) - const { access_token } = response.data - this.token = access_token - localStorage.setItem('token', access_token) + return response.data + } - await this.fetchUser() + async requestVerifyToken(email) { + const response = await axios.post('/api/auth/request-verify-token', { + email, + }) return response.data - } +} - async forgotPassword(email) { - const response = await axios.post(`api/auth/forgot-password`, { - email, + async verifyEmail(token) { + const response = await axios.post('/api/auth/verify', { + token, }) return response.data - } +} - async resetPassword(token, password) { - const response = await axios.post(`api/auth/reset-password`, { - token, - password, - }) + async login(email, password) { + const formData = new URLSearchParams() + formData.append('username', email) + formData.append('password', password) - return response.data - } - - async fetchUser() { - if (!this.token) return null - - try { - const response = await axios.get(`api/users/me`, { - headers: { - Authorization: `Bearer ${this.token}`, - }, - }) - this.user = response.data - localStorage.setItem('user', JSON.stringify(response.data)) - return response.data - } catch (error) { - this.logout() - throw error + const response = await axios.post(`api/auth/login`, formData, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + + const {access_token} = response.data + this.token = access_token + localStorage.setItem('token', access_token) + + await this.fetchUser() + + return response.data } - } - - async logout() { - try { - if (this.token) { - await axios.post(`api/auth/logout`, null, { - headers: { - Authorization: `Bearer ${this.token}`, - }, + + async forgotPassword(email) { + const response = await axios.post(`api/auth/forgot-password`, { + email, }) - } - } catch (error) { - console.error('Logout error:', error) - } finally { - this.token = null - this.user = null - localStorage.removeItem('token') - localStorage.removeItem('user') + + return response.data } - } - isAuthenticated() { - return !!this.token - } + async resetPassword(token, password) { + const response = await axios.post(`api/auth/reset-password`, { + token, + password, + }) - getUser() { - return this.user - } + return response.data + } - setupAxiosInterceptor() { - axios.interceptors.request.use( - config => { - if (this.token) { - config.headers.Authorization = `Bearer ${this.token}` + async fetchUser() { + if (!this.token) return null + + try { + const response = await axios.get(`api/users/me`, { + headers: { + Authorization: `Bearer ${this.token}`, + }, + }) + this.user = response.data + localStorage.setItem('user', JSON.stringify(response.data)) + return response.data + } catch (error) { + this.logout() + throw error } - return config - }, - error => Promise.reject(error) - ) - - axios.interceptors.response.use( - response => response, - error => { - if (error.response?.status === 401) { - this.logout() - window.location.href = '/login' + } + + async logout() { + try { + if (this.token) { + await axios.post(`api/auth/logout`, null, { + headers: { + Authorization: `Bearer ${this.token}`, + }, + }) + } + } catch (error) { + console.error('Logout error:', error) + } finally { + this.token = null + this.user = null + localStorage.removeItem('token') + localStorage.removeItem('user') } - return Promise.reject(error) - } - ) - } + } + + isAuthenticated() { + return !!this.token + } + + getUser() { + return this.user + } + + setupAxiosInterceptor() { + axios.interceptors.request.use( + config => { + if (this.token) { + config.headers.Authorization = `Bearer ${this.token}` + } + return config + }, + error => Promise.reject(error) + ) + + axios.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401) { + this.logout() + window.location.href = '/login' + } + return Promise.reject(error) + } + ) + } } export default new AuthService() From d78507b52a6b141305c187e170d6da78916a7772 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 16:01:35 +0300 Subject: [PATCH 18/31] new routers --- frontend/src/router/index.js | 98 ++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 00c3b61..52182eb 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,4 +1,4 @@ -import { createRouter, createWebHistory } from 'vue-router' +import {createRouter, createWebHistory} from 'vue-router' import authService from '../services/auth' import HomeView from '../components/Home.vue' @@ -6,59 +6,71 @@ import LoginView from '../components/Login.vue' import RegisterView from '../components/Register.vue' import ForgotPasswordView from '../components/ForgotPassword.vue' import ResetPasswordView from '../components/ResetPassword.vue' +import VerifyEmail from "../components/VerifyEmail.vue" +import CheckEmail from "../components/CheckEmail.vue"; const routes = [ - { - path: '/', - name: 'Home', - component: HomeView, - meta: { requiresAuth: true }, - }, - { - path: '/login', - name: 'Login', - component: LoginView, - meta: { guest: true }, - }, - { - path: '/register', - name: 'Register', - component: RegisterView, - meta: { guest: true }, - }, - { - path: '/forgot-password', - name: 'ForgotPassword', - component: ForgotPasswordView, - meta: { guest: true }, - }, - { - path: '/reset-password', - name: 'ResetPassword', - component: ResetPasswordView, - meta: { guest: true }, - }, + { + path: '/', + name: 'Home', + component: HomeView, + meta: {requiresAuth: true}, + }, + { + path: '/login', + name: 'Login', + component: LoginView, + meta: {guest: true}, + }, + { + path: '/register', + name: 'Register', + component: RegisterView, + meta: {guest: true}, + }, + { + path: '/forgot-password', + name: 'ForgotPassword', + component: ForgotPasswordView, + meta: {guest: true}, + }, + { + path: '/reset-password', + name: 'ResetPassword', + component: ResetPasswordView, + meta: {guest: true}, + }, + { + path: '/check-email', + name: 'CheckEmail', + component: CheckEmail + }, + { + path: '/verify', + name: 'VerifyEmail', + component: VerifyEmail + } ] const router = createRouter({ - history: createWebHistory(), - routes, + history: createWebHistory(), + routes, }) router.beforeEach((to, from, next) => { - const isAuthenticated = authService.isAuthenticated() + const isAuthenticated = authService.isAuthenticated() - if (to.meta.requiresAuth && !isAuthenticated) { - next('/login') - return - } + if (to.meta.requiresAuth && !isAuthenticated) { + next('/login') + return + } - if (to.meta.guest && isAuthenticated) { - next('/') - return - } + if (to.meta.guest && isAuthenticated) { + next('/') + return + } - next() + next() }) export default router From eef739eb7abc252f6bf41da29fa95cac424b54d5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 16:02:05 +0300 Subject: [PATCH 19/31] new pages: Check email, verify email --- frontend/src/components/CheckEmail.vue | 62 +++++++++++ frontend/src/components/VerifyEmail.vue | 134 ++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 frontend/src/components/CheckEmail.vue create mode 100644 frontend/src/components/VerifyEmail.vue diff --git a/frontend/src/components/CheckEmail.vue b/frontend/src/components/CheckEmail.vue new file mode 100644 index 0000000..5ed7a6a --- /dev/null +++ b/frontend/src/components/CheckEmail.vue @@ -0,0 +1,62 @@ + + + diff --git a/frontend/src/components/VerifyEmail.vue b/frontend/src/components/VerifyEmail.vue new file mode 100644 index 0000000..e78c7d8 --- /dev/null +++ b/frontend/src/components/VerifyEmail.vue @@ -0,0 +1,134 @@ + + + From fc378f1ffefef02fa6d1822bc640f0db5a925a1d Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 16:02:21 +0300 Subject: [PATCH 20/31] add redirect to verify email --- frontend/src/components/Register.vue | 72 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/Register.vue b/frontend/src/components/Register.vue index baccaa8..d868a8d 100644 --- a/frontend/src/components/Register.vue +++ b/frontend/src/components/Register.vue @@ -18,15 +18,15 @@ Email

{{ errors.email }}

@@ -37,15 +37,15 @@ Password

{{ errors.password }}

Minimum 8 characters

@@ -57,15 +57,15 @@ Confirm Password

{{ errors.confirmPassword }} @@ -86,9 +86,9 @@ @@ -171,14 +171,16 @@ export default { await authService.register(this.email, this.password) this.success = true - // Redirect to login after 2 seconds - setTimeout(() => { - this.$router.push('/login') - }, 2000) + this.$router.push({ + path: '/check-email', + query: { email: this.email } + }) + } catch (error) { console.error('Registration error:', error) this.error = - error.response?.data?.detail || 'Registration failed. Please try again.' + error.response?.data?.detail || + 'Registration failed. Please try again.' } finally { this.loading = false } From 47ff15897433802e5fc3f36edac01f26b6a63b05 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Fri, 13 Feb 2026 16:02:58 +0300 Subject: [PATCH 21/31] frontend url for link --- .../backlog_app/servicies/authentification/user_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index dcb009f..801e344 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -35,9 +35,10 @@ async def on_after_request_verify( "Verification requested for user %s. Verification token: %s", user.id, token ) - verification_link = ( - "http://127.0.0.1:8000/docs#/Auth/verify_verify_api_auth_verify_post" - ) + origin = request.headers.get("origin") or settings.FRONTEND_URL + logger.debug("origin url: %s", origin) + + verification_link = f"{origin}/verify?token={token}" await email_task.send_verification_email.kiq( user_id=str(user.id), user_email=user.email, From ef4df4c2ef9038eb223cb997f2e13c5d552c2cd4 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 12:11:46 +0300 Subject: [PATCH 22/31] edit email verification templates --- backend/backlog_app/templates/base.html | 8 -- .../email-verify/email-verified.html | 60 +++++++++++++-- .../email-verify/verification-request.html | 76 ++++++++++++++++--- 3 files changed, 120 insertions(+), 24 deletions(-) diff --git a/backend/backlog_app/templates/base.html b/backend/backlog_app/templates/base.html index 733bd64..14b7b00 100644 --- a/backend/backlog_app/templates/base.html +++ b/backend/backlog_app/templates/base.html @@ -10,13 +10,5 @@ {% block main %} {% endblock %} -

- {% block footer %} - Your site admin, -
- Backlog © 2025. - {% endblock %} -
- diff --git a/backend/backlog_app/templates/email-verify/email-verified.html b/backend/backlog_app/templates/email-verify/email-verified.html index 4d4ecfa..df43ffd 100644 --- a/backend/backlog_app/templates/email-verify/email-verified.html +++ b/backend/backlog_app/templates/email-verify/email-verified.html @@ -5,10 +5,58 @@ {% endblock %} {% block main %} -

- Dear, {{ user_email }}, -

-

- Email confirmed. Your email address has been verified. -

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ ✅ Email successfully confirmed +

+
+ Hello {{ user_email }}, +
+ Your email address has been successfully verified. + Your account is now fully activated and ready to use. +
+ + Go to Login + +
+ If you did not perform this action, please contact support immediately. +
+
{% endblock %} diff --git a/backend/backlog_app/templates/email-verify/verification-request.html b/backend/backlog_app/templates/email-verify/verification-request.html index 1235950..8f33409 100644 --- a/backend/backlog_app/templates/email-verify/verification-request.html +++ b/backend/backlog_app/templates/email-verify/verification-request.html @@ -1,17 +1,73 @@ {% extends 'base.html' %} {% block title %} - Email verification request + Email verification {% endblock %} {% block main %} -

- Dear, {{user_email}}, - Please verify your email for site.com at {{verification_link}} -

-

- Use this token to verify your email: -
- {{verification_token}} -

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Confirm your email +

+
+ Hello {{ user_email }}, +
+ Thanks for registering. Please confirm your email address by clicking the button below. +
+ + Verify Email + +
+ If the button doesn’t work, copy and paste this link into your browser: +
+ + {{ verification_link }} + +
+ This link will expire in 24 hours.
+ If you didn’t request this, you can safely ignore this email. +
+
{% endblock %} From c56b6703ec6972d4d845fc5d5804a09b025ba705 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 12:12:21 +0300 Subject: [PATCH 23/31] refactor email tasks --- .../servicies/authentification/user_manager.py | 3 --- backend/backlog_app/tasks/email_task.py | 10 +--------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index 801e344..9dd9ecd 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -40,9 +40,7 @@ async def on_after_request_verify( verification_link = f"{origin}/verify?token={token}" await email_task.send_verification_email.kiq( - user_id=str(user.id), user_email=user.email, - verification_token=token, verification_link=verification_link, ) @@ -50,6 +48,5 @@ async def on_after_verify(self, user: User, request: Optional["Request"] = None) logger.warning("User %s has been verified", user.id) await email_task.send_email_confirmed.kiq( - user_id=str(user.id), user_email=user.email, ) diff --git a/backend/backlog_app/tasks/email_task.py b/backend/backlog_app/tasks/email_task.py index 68c3d27..9022325 100644 --- a/backend/backlog_app/tasks/email_task.py +++ b/backend/backlog_app/tasks/email_task.py @@ -7,28 +7,22 @@ @broker.task async def send_verification_email( - user_id: str, user_email: str, - verification_token: str, verification_link: str, ) -> None: - subject = "Confirm your email for site.com" + subject = "Confirm your email" plain_content = dedent(f"""\ Dear {user_email}, Please verify your email for site.com at {verification_link}. - Use this token to verify your email: {verification_token} - Your site admin, 2025 """) template = templates.get_template("email-verify/verification-request.html") context = { - "user_id": user_id, "user_email": user_email, "verification_link": verification_link, - "verification_token": verification_token, } html_content = template.render(context) @@ -42,7 +36,6 @@ async def send_verification_email( @broker.task async def send_email_confirmed( - user_id: str, user_email: str, ): subject = "Email Confirmed" @@ -54,7 +47,6 @@ async def send_email_confirmed( 2025""") template = templates.get_template("email-verify/email-verified.html") context = { - "user_id": user_id, "user_email": user_email, } html_content = template.render(context) From cb87e0d9e4ca5f609d48b88f604f1ecca886aaf5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:28:40 +0300 Subject: [PATCH 24/31] add new tasks for send email if user forgot password --- backend/backlog_app/tasks/__init__.py | 2 + backend/backlog_app/tasks/email_task.py | 53 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/backend/backlog_app/tasks/__init__.py b/backend/backlog_app/tasks/__init__.py index b44a03d..7560f94 100644 --- a/backend/backlog_app/tasks/__init__.py +++ b/backend/backlog_app/tasks/__init__.py @@ -1,2 +1,4 @@ from .email_task import send_verification_email from .email_task import send_email_confirmed +from .email_task import send_email_forgot_password +from .email_task import send_email_forgot_password_confirmed diff --git a/backend/backlog_app/tasks/email_task.py b/backend/backlog_app/tasks/email_task.py index 9022325..338e9aa 100644 --- a/backend/backlog_app/tasks/email_task.py +++ b/backend/backlog_app/tasks/email_task.py @@ -57,3 +57,56 @@ async def send_email_confirmed( plain_content=plain_content, html_content=html_content, ) + + +@broker.task +async def send_email_forgot_password( + user_email: str, + reset_link: str, + token_lifetime: str +): + subject = "Request for change password" + plain_content = dedent(f"""\ + Dear {user_email}, + We get your request for change password. + Link for change: {reset_link} + Your site admin, + 2025""") + template = templates.get_template("email-forgot/password-reset-request.html") + context = { + "user_email": user_email, + "reset_link": reset_link, + "expires_in": token_lifetime, + } + html_content = template.render(context) + + await send_email( + recipient=user_email, + subject=subject, + plain_content=plain_content, + html_content=html_content, + ) + + +@broker.task +async def send_email_forgot_password_confirmed( + user_email: str, +): + subject = "Password reset request confirmed" + plain_content = dedent(f"""\ + Dear {user_email}, + Your password reset request has been confirmed. + Your site admin, + 2025""") + template = templates.get_template("email-forgot/password-reset-confirmed.html") + context = { + "user_email": user_email, + } + html_content = template.render(context) + + await send_email( + recipient=user_email, + subject=subject, + plain_content=plain_content, + html_content=html_content, + ) From 8d2ec68a6041bd8efbd07b489f4c09fb57e81a3c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:28:56 +0300 Subject: [PATCH 25/31] password reset templates --- .../password-reset-confirmed.html | 71 ++++++++++++++++ .../email-forgot/password-reset-request.html | 84 +++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 backend/backlog_app/templates/email-forgot/password-reset-confirmed.html create mode 100644 backend/backlog_app/templates/email-forgot/password-reset-request.html diff --git a/backend/backlog_app/templates/email-forgot/password-reset-confirmed.html b/backend/backlog_app/templates/email-forgot/password-reset-confirmed.html new file mode 100644 index 0000000..a54c383 --- /dev/null +++ b/backend/backlog_app/templates/email-forgot/password-reset-confirmed.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} + +{% block title %} + Password successfully changed +{% endblock %} + +{% block main %} + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ 🔑 Password changed successfully +

+
+ Hello {{ user_email }}, +
+ Your password has been successfully updated. You can now use your new password to log in to your account. +
+ + Log in to your account + +
+ If you did not perform this action, please contact our support immediately. +
+ This is an automated message. Please do not reply to this email. +
+
+{% endblock %} diff --git a/backend/backlog_app/templates/email-forgot/password-reset-request.html b/backend/backlog_app/templates/email-forgot/password-reset-request.html new file mode 100644 index 0000000..553f684 --- /dev/null +++ b/backend/backlog_app/templates/email-forgot/password-reset-request.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} + +{% block title %} + Password reset request +{% endblock %} + +{% block main %} + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ 🔐 Password reset request +

+
+ Hello {{ user_email }}, +
+ We received a request to reset your account password. + Click the button below to set a new password. +
+ + Reset Password + +
+ If the button doesn’t work, copy and paste this link into your browser: +
+ + {{ reset_link }} + +
+ If you did not request a password reset, please ignore this email. + Your password will remain unchanged. +
+ This link will expire in {{ expires_in | default("24 hours") }}. +
+
+{% endblock %} From fa665af6bc64ac921413e0fe90fb5714869b38d2 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:29:20 +0300 Subject: [PATCH 26/31] email formater for lifetime link --- .../backlog_app/servicies/mailing/__init__.py | 2 ++ .../servicies/mailing/email_formater.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 backend/backlog_app/servicies/mailing/email_formater.py diff --git a/backend/backlog_app/servicies/mailing/__init__.py b/backend/backlog_app/servicies/mailing/__init__.py index e69de29..03dd577 100644 --- a/backend/backlog_app/servicies/mailing/__init__.py +++ b/backend/backlog_app/servicies/mailing/__init__.py @@ -0,0 +1,2 @@ +from .email_sender import send_email +from .email_formater import format_seconds_for_email diff --git a/backend/backlog_app/servicies/mailing/email_formater.py b/backend/backlog_app/servicies/mailing/email_formater.py new file mode 100644 index 0000000..fe912ed --- /dev/null +++ b/backend/backlog_app/servicies/mailing/email_formater.py @@ -0,0 +1,19 @@ + +def format_seconds_for_email(seconds: int) -> str: + """ + Converts a number of seconds into a human-readable string for emails. + + Examples: + 3600 -> "1 hour" + 5400 -> "1 hour 30 min" + 2700 -> "45 min" + """ + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + + if hours > 0 and minutes == 0: + return f"{hours} hour{'s' if hours > 1 else ''}" + elif hours > 0 and minutes > 0: + return f"{hours} hour{'s' if hours > 1 else ''} {minutes} min" + else: + return f"{minutes} min" From 0ba46cccf91692da52319c56347bb0d8e0ef129a Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:30:23 +0300 Subject: [PATCH 27/31] add new methods to user manager --- .../authentification/user_manager.py | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index 9dd9ecd..bc995c9 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -4,12 +4,13 @@ from fastapi_users import BaseUserManager, UUIDIDMixin +from backlog_app.servicies.mailing import format_seconds_for_email from backlog_app.config import settings from backlog_app.models import User from backlog_app.tasks import email_task if TYPE_CHECKING: - from fastapi import Request + from fastapi import Request, Response logger = logging.getLogger(__name__) @@ -19,20 +20,15 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): verification_token_secret = settings.access_token_db.verification_token_secret async def on_after_register(self, user: User, request: Optional["Request"] = None): - logger.warning("User %s has registered.", user.id) - - async def on_after_forgot_password( - self, user: User, token: str, request: Optional["Request"] = None - ): - logger.warning( - "User %s has forgot their password. Reset token: $s", user.id, token - ) + logger.info("User <%s> has registered.", user.id) async def on_after_request_verify( self, user: User, token: str, request: Optional["Request"] = None ): - logger.warning( - "Verification requested for user %s. Verification token: %s", user.id, token + logger.debug( + "Verification requested for user <%s>. Verification token: %s", + user.id, + token, ) origin = request.headers.get("origin") or settings.FRONTEND_URL @@ -45,8 +41,46 @@ async def on_after_request_verify( ) async def on_after_verify(self, user: User, request: Optional["Request"] = None): - logger.warning("User %s has been verified", user.id) + logger.warning("User <%s> has been verified", user.id) await email_task.send_email_confirmed.kiq( user_email=user.email, ) + + async def on_after_login( + self, + user: User, + request: Optional["Request"] = None, + response: Optional["Response"] | None = None, + ) -> None: + logger.warning("User <%s> has logged in", user.id) + + async def on_after_forgot_password( + self, user: User, token: str, request: Optional["Request"] = None + ): + logger.debug( + "User <%s> has forgot their password. Reset token: %s, lifetime: %s", + user.id, + token, + self.reset_password_token_lifetime_seconds, + ) + origin = request.headers.get("origin") or settings.FRONTEND_URL + reset_link = f"{origin}/forgot-password?token={token}" + token_lifetime = format_seconds_for_email( + self.reset_password_token_lifetime_seconds + ) + + await email_task.send_email_forgot_password.kiq( + user_email=user.email, + reset_link=reset_link, + token_lifetime=token_lifetime, + ) + + async def on_after_reset_password( + self, user: User, request: Optional["Request"] = None + ) -> None: + logger.warning("User <%s> successfully changed their password", user.id) + + await email_task.send_email_forgot_password_confirmed.kiq( + user_email=user.email, + ) From c6166752a2e1aa6958d4633fdc5e1a6df24e5807 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:41:34 +0300 Subject: [PATCH 28/31] fix redirect link --- backend/backlog_app/servicies/authentification/user_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index bc995c9..558b15f 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -65,7 +65,7 @@ async def on_after_forgot_password( self.reset_password_token_lifetime_seconds, ) origin = request.headers.get("origin") or settings.FRONTEND_URL - reset_link = f"{origin}/forgot-password?token={token}" + reset_link = f"{origin}/reset-password?token={token}" token_lifetime = format_seconds_for_email( self.reset_password_token_lifetime_seconds ) From 039f57eba35738b7ef147167b8eaf5c45c93e202 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:48:19 +0300 Subject: [PATCH 29/31] lint fix --- backend/backlog_app/config.py | 3 ++- .../servicies/authentification/user_manager.py | 2 +- backend/backlog_app/servicies/mailing/__init__.py | 2 +- .../backlog_app/servicies/mailing/email_formater.py | 1 - backend/backlog_app/taskiq_broker.py | 6 ++++-- backend/backlog_app/tasks/__init__.py | 10 ++++++---- backend/backlog_app/tasks/email_task.py | 4 +--- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/backlog_app/config.py b/backend/backlog_app/config.py index f40ca79..1cbf46a 100644 --- a/backend/backlog_app/config.py +++ b/backend/backlog_app/config.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Literal -from pydantic import BaseModel, field_validator, AmqpDsn +from pydantic import AmqpDsn, BaseModel, field_validator from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, @@ -54,6 +54,7 @@ def database_url_asyncpg(self): class DataBase(BaseModel): connection: DataBaseConnection + class TaskiqConfig(BaseModel): url: AmqpDsn = "amqp://guest:guest@localhost:5672//" diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index 558b15f..d0a9768 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -4,9 +4,9 @@ from fastapi_users import BaseUserManager, UUIDIDMixin -from backlog_app.servicies.mailing import format_seconds_for_email from backlog_app.config import settings from backlog_app.models import User +from backlog_app.servicies.mailing import format_seconds_for_email from backlog_app.tasks import email_task if TYPE_CHECKING: diff --git a/backend/backlog_app/servicies/mailing/__init__.py b/backend/backlog_app/servicies/mailing/__init__.py index 03dd577..405374e 100644 --- a/backend/backlog_app/servicies/mailing/__init__.py +++ b/backend/backlog_app/servicies/mailing/__init__.py @@ -1,2 +1,2 @@ -from .email_sender import send_email from .email_formater import format_seconds_for_email +from .email_sender import send_email diff --git a/backend/backlog_app/servicies/mailing/email_formater.py b/backend/backlog_app/servicies/mailing/email_formater.py index fe912ed..2e945b6 100644 --- a/backend/backlog_app/servicies/mailing/email_formater.py +++ b/backend/backlog_app/servicies/mailing/email_formater.py @@ -1,4 +1,3 @@ - def format_seconds_for_email(seconds: int) -> str: """ Converts a number of seconds into a human-readable string for emails. diff --git a/backend/backlog_app/taskiq_broker.py b/backend/backlog_app/taskiq_broker.py index bc06eb5..8793cb9 100644 --- a/backend/backlog_app/taskiq_broker.py +++ b/backend/backlog_app/taskiq_broker.py @@ -1,9 +1,10 @@ +import logging +import taskiq_fastapi from taskiq import TaskiqEvents, TaskiqState from taskiq_aio_pika import AioPikaBroker -import taskiq_fastapi + from backlog_app.config import settings -import logging logger = logging.getLogger(__name__) @@ -11,6 +12,7 @@ taskiq_fastapi.init(broker, "backlog_app.main:app") + @broker.on_event(TaskiqEvents.WORKER_STARTUP) async def on_worker_startup(state: TaskiqState) -> None: logging.basicConfig( diff --git a/backend/backlog_app/tasks/__init__.py b/backend/backlog_app/tasks/__init__.py index 7560f94..33fac74 100644 --- a/backend/backlog_app/tasks/__init__.py +++ b/backend/backlog_app/tasks/__init__.py @@ -1,4 +1,6 @@ -from .email_task import send_verification_email -from .email_task import send_email_confirmed -from .email_task import send_email_forgot_password -from .email_task import send_email_forgot_password_confirmed +from .email_task import ( + send_email_confirmed, + send_email_forgot_password, + send_email_forgot_password_confirmed, + send_verification_email, +) diff --git a/backend/backlog_app/tasks/email_task.py b/backend/backlog_app/tasks/email_task.py index 338e9aa..6a46bfb 100644 --- a/backend/backlog_app/tasks/email_task.py +++ b/backend/backlog_app/tasks/email_task.py @@ -61,9 +61,7 @@ async def send_email_confirmed( @broker.task async def send_email_forgot_password( - user_email: str, - reset_link: str, - token_lifetime: str + user_email: str, reset_link: str, token_lifetime: str ): subject = "Request for change password" plain_content = dedent(f"""\ From 031535146d34522f3874ed4f4c8a1be493c5dc45 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 13:58:22 +0300 Subject: [PATCH 30/31] update project tech --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f8c77a9..cdef7b7 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Backlog — сервис «списка хотелок» (backlog) для фил - Alembic (миграции) - PostgreSQL - taskiq (фоновые задачи) +- RabbitMQ - Jinja2 ### Frontend From 9aab21552a563732157e036b37f458d4f4006b44 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Tue, 17 Feb 2026 14:04:55 +0300 Subject: [PATCH 31/31] switching the parameter to require mail verification --- backend/backlog_app/api/view/auth_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/backlog_app/api/view/auth_view.py b/backend/backlog_app/api/view/auth_view.py index 3ff52a9..b3f6889 100644 --- a/backend/backlog_app/api/view/auth_view.py +++ b/backend/backlog_app/api/view/auth_view.py @@ -14,7 +14,7 @@ router.include_router( fastapi_users.get_auth_router( authentication_backend, - # requires_verification=True, # It is not necessary to include the parameter in this application. + requires_verification=True, # It is not necessary to include the parameter in this application. ) )