Skip to content
This repository has been archived by the owner on Apr 14, 2024. It is now read-only.

part of change_password logic #249

Merged
merged 16 commits into from
Mar 25, 2024
6 changes: 5 additions & 1 deletion sapphire/common/broker/models/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from pydantic import BaseModel

from sapphire.common.utils import empty


class EmailType(str, enum.Enum):
PARTICIPANT_REQUESTED = "participant_requested"
Expand All @@ -11,8 +13,10 @@ class EmailType(str, enum.Enum):
PARTICIPANT_DECLINED = "participant_declined"
PARTICIPANT_LEFT = "participant_left"
OWNER_EXCLUDED = "owner_excluded"
CHANGE_PASSWORD = "change_password"
brulitsan marked this conversation as resolved.
Show resolved Hide resolved


class Email(BaseModel):
type: EmailType
to: list[uuid.UUID]
to: list[str]
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
sending_data: dict = {}
11 changes: 2 additions & 9 deletions sapphire/email/sender/service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import uuid
from typing import Any, Iterable

import aiosmtplib
Expand Down Expand Up @@ -35,16 +34,10 @@ def __init__(
def templates(self) -> dict[str, Template]:
return self._templates

async def _get_recipient_email(self, recipient: uuid.UUID) -> str:
# Issue: Write a function to get email from the users service using user_id

return "email@example.com"

async def send(self, template: Template, data: dict[str, Any], recipients: Iterable[uuid.UUID]):
async def send(self, template: Template, data: dict[str, Any], recipients: Iterable[str]):
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
coroutines = []
for recipient in recipients:
recipient_email = await self._get_recipient_email(recipient)
message = template.render(recipient=recipient_email, sender=self._sender, data=data)
message = template.render(recipient=recipient, sender=self._sender, data=data)
coroutine = backoff.on_exception(backoff.expo, Exception, max_tries=3)(
self._client.send_message,
)(message)
Expand Down
14 changes: 7 additions & 7 deletions sapphire/projects/broker/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async def send_participant_requested(
) -> None:
"""RECIPIENTS: ONLY OWNER"""
await self._send_email(
recipients=[project.owner_id],
recipients=[owner_email],
email_type=EmailType.PARTICIPANT_REQUESTED,
)

Expand All @@ -50,7 +50,7 @@ async def send_participant_joined(
) -> None:
"""RECIPIENTS: PARTICIPANTS"""
await self._send_email(
recipients=[p.user_id for p in project.joined_participants],
recipients=[p.user.email for p in project.joined_participants],
email_type=EmailType.PARTICIPANT_JOINED,
)

Expand All @@ -74,7 +74,7 @@ async def send_participant_declined(
) -> None:
"""RECIPIENTS: ONLY OWNER"""
await self._send_email(
recipients=[project.owner_id], email_type=EmailType.PARTICIPANT_DECLINED
recipients=[owner_email], email_type=EmailType.PARTICIPANT_DECLINED
)

await self._send_notification_to_recipients(
Expand All @@ -97,7 +97,7 @@ async def send_owner_declined(
) -> None:
"""RECIPIENTS: ONLY PARTICIPANT"""
await self._send_email(
recipients=[participant.user_id], email_type=EmailType.OWNER_DECLINED
recipients=[participant.user.email], email_type=EmailType.OWNER_DECLINED
)

await self._send_notification_to_recipients(
Expand All @@ -120,7 +120,7 @@ async def send_participant_left(
) -> None:
"""RECIPIENTS: PROJECT OWNER AND PARTICIPANTS"""
await self._send_email(
recipients=[project.owner_id] + [p.user_id for p in project.joined_participants],
recipients=[owner_email] + [p.user.email for p in project.joined_participants],
email_type=EmailType.PARTICIPANT_LEFT,
)

Expand All @@ -144,7 +144,7 @@ async def send_owner_excluded(
) -> None:
"""RECIPIENTS: PROJECT OWNER AND PARTICIPANTS"""
await self._send_email(
recipients=[project.owner_id] + [p.user_id for p in project.joined_participants],
recipients=[owner_email] + [p.user.email for p in project.joined_participants],
email_type=EmailType.OWNER_EXCLUDED,
)

Expand Down Expand Up @@ -176,7 +176,7 @@ async def _send_notification_to_recipients(self,
await asyncio.gather(*send_tasks)

async def _send_email(
self, recipients: list[uuid.UUID], email_type: EmailType, topic: str = "email"
self, recipients: list[str], email_type: EmailType, topic: str = "email"
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
):
await self.send(topic=topic, message=Email(to=recipients, type=email_type))

Expand Down
49 changes: 47 additions & 2 deletions sapphire/users/api/rest/auth/handlers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fastapi

from sapphire.common.api.exceptions import HTTPNotAuthenticated
from sapphire.common.api.exceptions import HTTPNotAuthenticated, HTTPNotFound, HTTPForbidden
from sapphire.common.jwt.dependencies.rest import get_jwt_data
from sapphire.common.jwt.methods import JWTMethods
from sapphire.common.jwt.models import JWTData
from sapphire.users import database
from sapphire.users import broker, cache, database

from .schemas import AuthorizeRequest, AuthorizeResponse
from .utils import generate_authorize_response
Expand Down Expand Up @@ -67,3 +67,48 @@ async def sign_in(
raise HTTPNotAuthenticated()

return generate_authorize_response(jwt_methods=jwt_methods, response=response, user=user)


async def change_password(
request: fastapi.Request,
email: str
):
broker_service: broker.Service = request.app.service.broker
database_service: database.Service = request.app.service.database
cache_service: cache.Service = request.app.service.cache

async with database_service.transaction() as session:
user = await database_service.get_user(
session=session,
email=email
)
if not user:
raise HTTPNotFound()

secret_code = await cache_service.change_password_set_secret_code() # in the future will be key
# to get code to validate sent code with input code
await broker_service.send_email_code(email=email, code=secret_code)

return fastapi.Response(status_code=200)


async def reset_password(
request: fastapi.Request,
secret_code: str,
email: str,
new_password: str
):
database_service: database.Service = request.app.service.database
cache_service: cache.Service = request.app.service.cache

if not cache_service.change_password_validate_code(secret_code=secret_code):
raise HTTPForbidden()

async with database_service.transaction() as session:
user = await database_service.get_user(session=session, email=email)
await database_service.update_user(
session=session,
user=user,
password=new_password
)
return fastapi.Response(status_code=200, content="Password reset")
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion sapphire/users/api/rest/auth/oauth2/habr.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def authorize(

cache_service: cache.Service = request.app.service.cache

state = await cache_service.set_state()
state = await cache_service.oauth_set_state()

if redirect_url is None:
redirect_url = oauth2_habr_callback_url
Expand Down
2 changes: 2 additions & 0 deletions sapphire/users/api/rest/auth/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
router.add_api_route(methods=["POST"], path="/signup", endpoint=handlers.sign_up)
router.add_api_route(methods=["POST"], path="/signin", endpoint=handlers.sign_in)
router.include_router(oauth2.router, prefix="/oauth2")
router.add_api_route(methods=["POST"], path="/change_password", endpoint=handlers.change_password)
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
router.add_api_route(methods=['POST'], path="/reset_password", endpoint=handlers.reset_password)
2 changes: 2 additions & 0 deletions sapphire/users/broker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .service import Service, get_service
from .settings import Settings
24 changes: 24 additions & 0 deletions sapphire/users/broker/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import asyncio
from sapphire.common.broker.models.email import Email, EmailType
from sapphire.common.broker.service import BaseBrokerProducerService

from . import Settings


class Service(BaseBrokerProducerService):
async def send_email_code(self, email: str, code: str, topic: str = "email"):
await self.send(
topic=topic,
message=Email(
to=[email],
type=EmailType.CHANGE_PASSWORD,
sending_data={'code': code}
)
)


def get_service(
loop: asyncio.AbstractEventLoop,
settings: Settings,
) -> Service:
return Service(loop=loop, servers=settings.servers)
5 changes: 5 additions & 0 deletions sapphire/users/broker/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sapphire.common.broker.settings import BaseBrokerProducerSettings


class Settings(BaseBrokerProducerSettings):
servers: list[str] = ["localhost:9091"]
19 changes: 17 additions & 2 deletions sapphire/users/cache/service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import secrets
import uuid

from sapphire.common.cache.service import BaseCacheService
Expand All @@ -6,20 +7,34 @@


class Service(BaseCacheService):
async def set_state(self) -> str:
async def oauth_set_state(self) -> str:
state = str(uuid.uuid4())
key = f"users:auth:oauth2:habr:state:{state}"
await self.redis.set(key, state, ex=120)
return state

async def validate_state(self, state: str) -> bool:
async def oauth_validate_state(self, state: str) -> bool:
key = f"users:auth:oauth2:habr:state:{state}"
value = await self.redis.get(key)
if value is not None:
await self.redis.delete(key)
return True
return False

async def change_password_set_secret_code(self) -> str:
secret_code = str(secrets.token_urlsafe(12))
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
key = f"users:auth:change_password:secret_code:{secret_code}"
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
await self.redis.set(key, secret_code, ex=43200)
brulitsan marked this conversation as resolved.
Show resolved Hide resolved
return secret_code

async def change_password_validate_code(self, secret_code: str) -> bool:
key = f"users:auth:change_password:secret_code:{secret_code}"
value = await self.redis.get(key)
if value is not None:
await self.redis.delete(key)
return True
return False


def get_service(settings: Settings) -> Service:
return Service(url=str(settings.url))
2 changes: 2 additions & 0 deletions sapphire/users/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .cache import Settings as CacheSettings
from .database import Settings as DatabaseSettings
from .oauth2.habr import Settings as OAuth2HabrSettings
from .broker import Settings as BrokerSettings


class Settings(BaseModel):
Expand All @@ -18,3 +19,4 @@ class Settings(BaseModel):
habr: HabrSettings = HabrSettings()
habr_career: HabrCareerSettings = HabrCareerSettings()
oauth2_habr: OAuth2HabrSettings = OAuth2HabrSettings()
broker: BrokerSettings = BrokerSettings()